-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathconfig.go
383 lines (320 loc) · 9.88 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package main
import (
"encoding/json"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
)
// Channel is IPTV channel in configuration
type Channel struct {
// name of the channel, such as 'CCTV-1'
Name string `json:"name"`
// display name of the channel, such as 'CCTV-1综合'
DisplayName string `json:"displayName,omitempty"`
// logo is the logo of the channel
Logo string `json:"logo,omitempty"`
// hiden channels are not shown in the channel list
Hide bool `json:"hide,omitempty"`
// sources of the channel, if a source does NOT begin with 'http',
// MyIPTV regards it as a multicast address.
Sources []string `json:"sources,omitempty"`
}
// ChannelGroup is a group of IPTV channels
type ChannelGroup struct {
// name of the channel group, such as 'CCTV' or '央视'
Name string `json:"name"`
// channels in the group
Channels []Channel `json:"channels,omitempty"`
}
// Config defines MyIPTV configuration
type Config struct {
// HTTP server address, including IP address and port
ServerAddr string `json:"serverAddr,omitempty"`
// EPG URL, default is 'http://epg.51zmt.top:8000/e.xml'
EPGURL string `json:"epgURL,omitempty"`
// name of the multicast interface
McastIface string `json:"mcastIface,omitempty"`
// McastPacketSize is the buffer size for one multicast packet,
// default is 2048
McastPacketSize int `json:"mcastPacketSize,omitempty"`
// WriteBufferSize is the buffer size, which is used to buffer the
// multicast packets before writing to clients, default is 131072.
// Note changing this value may not take effect immediately, beacuse
// we are using sync.Pool to manage the buffers.
WriteBufferSize int `json:"writeBufferSize,omitempty"`
// ReadTimeout is the timeout for read multicast packets to fill the write
// buffer, its unit is millisecond, default is 1000
ReadTimeout int `json:"readTimeout,omitempty"`
}
// Clourflare DDNS configuration
type DDNSConfig struct {
// RecordName is the domain name to update, for example: 'blog.localvar.cn'
RecordName string `json:"recordName"`
// ZoneID is the zone ID of Cloudflare
ZoneID string `json:"zoneID"`
// APIKey is the Cloudflare APIKey
APIKey string `json:"apiKey"`
// WANIPProviders is a list of URLs to get the WAN IP address, for example:
//
// ["http://ipv4.icanhazip.com", "https://whatismyip.akamai.com"]
//
// Note if the provider returns an IPV4 address, the program will update
// the A record of the domain name, if the provider returns an IPV6 address,
// the program will update the AAAA record.
WANIPProviders []string `json:"wanIPProviders"`
// DNSServers is an optional list of DNS servers to resolve the domain name,
// the servers must include the port number (typically 53), if set, the
// program will use these DNS servers to speed up the resolution, for
// example: ["beth.ns.cloudflare.com:53", "rudy.ns.cloudflare.com:53"]
DNSServers []string `json:"dnsServers,omitempty"`
}
// findBestIP finds the best IP address from the http server
func findBestIP(m map[string][]string) string {
first := ""
for _, ips := range m {
for _, ip := range ips {
// we use the first IP by default
if first == "" {
first = ip
}
// but if there's a 192.168.x.x IP, we use it
if strings.HasPrefix(ip, "192.168.") {
return ip
}
}
}
return first
}
// populateDefault populates default values to the configuration.
func (cfg *Config) populateDefault() {
if cfg.EPGURL == "" {
cfg.EPGURL = "http://epg.51zmt.top:8000/e.xml"
}
if cfg.McastPacketSize <= 0 {
cfg.McastPacketSize = 2048
}
if cfg.WriteBufferSize <= 0 {
cfg.WriteBufferSize = 131072
}
if cfg.ReadTimeout <= 0 {
cfg.ReadTimeout = 1000
}
if cfg.ServerAddr != "" && cfg.McastIface != "" {
return
}
// next, try to find the best network interface and IP address
// for the multicast interface and the http server
m := GetInterfacesAndIPs()
// If no network interface is found and server address is not configured
// use '0.0.0.0:7709'. Note this may not work, and even if the http server
// could be started, the generated IPTV relay addresses may not be
// accessible from other devices.
if len(m) == 0 {
if cfg.ServerAddr == "" {
cfg.ServerAddr = "0.0.0.0:7709"
}
return
}
if len(m) == 1 {
if cfg.ServerAddr == "" {
cfg.ServerAddr = findBestIP(m) + ":7709"
} else if cfg.McastIface == "" {
for iface := range m {
cfg.McastIface = iface
}
}
return
}
// if multicast interface is configured, remove it so that the http server
// won't use it
if cfg.McastIface != "" {
delete(m, cfg.McastIface)
}
// try configure the http server address
if cfg.ServerAddr == "" {
cfg.ServerAddr = findBestIP(m) + ":7709"
}
// try configure the multicast interface
if cfg.McastIface == "" {
IFACE_LOOP:
for iface, ips := range m {
for _, ip := range ips {
// remove the interface if it has the http server address
if strings.HasPrefix(cfg.ServerAddr, ip+":") {
delete(m, iface)
break IFACE_LOOP
}
}
}
for iface := range m {
cfg.McastIface = iface
break
}
}
}
var (
// configPath is the path of the configuration file, it may be updated
// in function 'loadConfig'.
configPath = "myiptv.json"
// ddnsConfig is the Cloudflare DDNS configuration,
// it won't be changed after the program starts.
ddnsConfig *DDNSConfig = nil
// config is the current configuration, it's an atomic value which
// points to a Config object.
config atomic.Value
channelGroups []ChannelGroup
// though we use atomic.Value to store the configuration, we still need
// the lock, because we need to update the configuration file.
configLock sync.Mutex
)
// getConfig returns the current configuration.
func getConfig() *Config {
return config.Load().(*Config)
}
// getDDNSConfig returns the DDNS configuration.
func getDDNSConfig() *DDNSConfig {
return ddnsConfig
}
// allConfig is a helper structure to help load and save configuration,
// because we use a single file to store both configuration and channel
// groups.
type allConfig struct {
DDNS *DDNSConfig `json:"ddns,omitempty"`
Config *Config `json:"config"`
ChannelGroups []ChannelGroup `json:"channelGroups,omitempty"`
}
// loadConfig loads configuration from 'myiptv.json', it ignores all errors,
// default values are used if there's an error, or any item is missing.
func loadConfig() {
// try to find the configuration file in the current directory, if not
// found, try to find it in the directory of the executable.
if fi, err := os.Stat(configPath); err != nil || fi.IsDir() {
if exePath, err := os.Executable(); err == nil {
path := filepath.Join(filepath.Dir(exePath), configPath)
if fi, err = os.Stat(path); err == nil && !fi.IsDir() {
configPath = path
}
} else {
slog.Error(
"failed to get executable path",
slog.String("error", err.Error()),
)
}
}
var allCfg allConfig
if f, err := os.Open(configPath); err == nil {
if err = json.NewDecoder(f).Decode(&allCfg); err != nil {
slog.Error(
"failed to decode configuration file",
slog.String("error", err.Error()),
)
}
f.Close()
} else {
slog.Error(
"failed to open configuration file",
slog.String("error", err.Error()),
)
}
cfg := allCfg.Config
if cfg == nil {
cfg = new(Config)
}
cfg.populateDefault()
config.Store(cfg)
// this function is only called at program startup, no need to lock
channelGroups = allCfg.ChannelGroups
ddnsConfig = allCfg.DDNS
}
// saveConfig saves the configuration to 'configPath'.
func saveConfig(cfg *Config, chGrps []ChannelGroup) error {
allCfg := allConfig{
DDNS: ddnsConfig,
Config: cfg,
ChannelGroups: chGrps,
}
data, err := json.Marshal(&allCfg)
if err != nil {
slog.Error(
"failed to marshal config",
slog.String("error", err.Error()),
)
return err
}
err = os.WriteFile(configPath, data, 0666)
if err != nil {
slog.Error(
"failed to write config to file",
slog.String("error", err.Error()),
)
return err
}
return nil
}
// apiGetConfig returns the current configuration
func apiGetConfig(w http.ResponseWriter, r *http.Request) {
cfg := getConfig()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cfg)
}
// apiUpdateConfig updates and saves the configuration
func apiUpdateConfig(w http.ResponseWriter, r *http.Request) {
var cfg Config
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
slog.Error(
"failed to decode request body",
slog.String("error", err.Error()),
)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
configLock.Lock()
defer configLock.Unlock()
if err := saveConfig(&cfg, channelGroups); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// call populateDefault after saving the configuration, because we don't
// want to save the populated default values.
cfg.populateDefault()
config.Store(&cfg)
}
// channelGroupForEach iterates all channel groups and calls the function.
func channelGroupForEach(fn func(*ChannelGroup)) {
configLock.Lock()
defer configLock.Unlock()
for i := range channelGroups {
fn(&channelGroups[i])
}
}
// apiListChannelGroups lists all channel groups
func apiListChannelGroups(w http.ResponseWriter, r *http.Request) {
_ = r
configLock.Lock()
defer configLock.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(channelGroups)
}
// apiUpdateChannelGroup updates the channel group
func apiUpdateChannelGroups(w http.ResponseWriter, r *http.Request) {
var chGrps []ChannelGroup
if err := json.NewDecoder(r.Body).Decode(&chGrps); err != nil {
slog.Error(
"failed to decode request body",
slog.String("error", err.Error()),
)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
configLock.Lock()
defer configLock.Unlock()
if err := saveConfig(getConfig(), chGrps); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
channelGroups = chGrps
}