1// Package merlin implements the ASUS-Merlin init system.
2
3package merlin
4
5import (
6	"bufio"
7	"bytes"
8	"fmt"
9	"io/ioutil"
10	"os"
11	"os/exec"
12	"strings"
13
14	"github.com/nextdns/nextdns/host/service"
15	"github.com/nextdns/nextdns/host/service/internal"
16)
17
18type Service struct {
19	service.Config
20	service.ConfigFileStorer
21	Path       string
22	JFFSScript string
23}
24
25func New(c service.Config) (Service, error) {
26	if b, err := exec.Command("uname", "-o").Output(); err != nil ||
27		!strings.HasPrefix(string(b), "ASUSWRT-Merlin") {
28		return Service{}, service.ErrNotSuported
29	}
30	ep, err := os.Executable()
31	if err != nil {
32		return Service{}, err
33	}
34	return Service{
35		Config:           c,
36		ConfigFileStorer: service.ConfigFileStorer{File: ep + ".conf"},
37		Path:             ep + ".init",
38		JFFSScript:       "/jffs/scripts/services-start",
39	}, nil
40}
41
42func (s Service) Install() error {
43	if err := internal.CreateWithTemplate(s.Path, tmpl, 0755, s.Config); err != nil {
44		return err
45	}
46
47	out, err := internal.RunOutput("nvram", "get", "jffs2_scripts")
48	if err != nil {
49		return fmt.Errorf("check jffs2_scripts: %v", err)
50	}
51	if !strings.HasPrefix(out, "1") {
52		if err := internal.Run("nvram", "set", "jffs2_scripts=1"); err != nil {
53			return fmt.Errorf("enable jffs2_scripts: %v", err)
54		}
55		if err := internal.Run("nvram", "commit"); err != nil {
56			return fmt.Errorf("nvram commit: %v", err)
57		}
58	}
59	if err := addLine(s.JFFSScript, s.Path+" start"); err != nil {
60		return err
61	}
62	return nil
63}
64
65func (s Service) Uninstall() error {
66	_ = removeLine(s.JFFSScript, s.Path+" start")
67	if err := os.Remove(s.Path); err != nil {
68		if os.IsNotExist(err) {
69			return service.ErrNoInstalled
70		}
71		return err
72	}
73	return nil
74}
75
76func (s Service) Status() (service.Status, error) {
77	if _, err := os.Stat(s.Path); os.IsNotExist(err) {
78		return service.StatusNotInstalled, nil
79	}
80
81	err := internal.Run(s.Path, "status")
82	if internal.ExitCode(err) == 1 {
83		return service.StatusStopped, nil
84	} else if err != nil {
85		return service.StatusUnknown, err
86	}
87	return service.StatusRunning, nil
88}
89
90func (s Service) Start() error {
91	return internal.Run(s.Path, "start")
92}
93
94func (s Service) Stop() error {
95	return internal.Run(s.Path, "stop")
96}
97
98func (s Service) Restart() error {
99	return internal.Run(s.Path, "restart")
100}
101
102func excludeLine(file, line string) (found bool, out []byte, err error) {
103	f, err := os.Open(file)
104	if err != nil {
105		return false, nil, err
106	}
107	defer f.Close()
108	s := bufio.NewScanner(f)
109	for s.Scan() {
110		if s.Text() == line {
111			found = true
112		} else {
113			out = append(out, s.Bytes()...)
114			out = append(out, '\n')
115		}
116	}
117	if err := s.Err(); err != nil {
118		return false, nil, err
119	}
120	return
121}
122
123func addLine(file, line string) error {
124	found, _, err := excludeLine(file, line)
125	if os.IsNotExist(err) {
126		return ioutil.WriteFile(file, []byte("#!/bin/sh\n"+line+"\n"), 0755)
127	}
128	if err != nil {
129		return err
130	}
131	if found {
132		return service.ErrAlreadyInstalled
133	}
134	b, err := ioutil.ReadFile(file)
135	if err != nil {
136		return err
137	}
138	f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
139	if err != nil {
140		return err
141	}
142	defer f.Close()
143	s := bufio.NewScanner(bytes.NewReader(b))
144	firstLine := true
145	for s.Scan() {
146		l := s.Text()
147		if firstLine {
148			firstLine = false
149			if strings.HasPrefix(l, "#!") {
150				_, err = fmt.Fprintf(f, "%s\n\n%s\n", l, line)
151			} else {
152				// missing shebang
153				_, err = fmt.Fprintf(f, "#!/bin/sh\n\n%s\n%s\n", line, l)
154			}
155		} else {
156			_, err = fmt.Fprintln(f, l)
157		}
158		if err != nil {
159			return err
160		}
161	}
162	if err := s.Err(); err != nil {
163		return err
164	}
165	if firstLine {
166		// Empty file
167		return ioutil.WriteFile(file, []byte("#!/bin/sh\n"+line+"\n"), 0755)
168	}
169	return err
170}
171
172func removeLine(file, line string) error {
173	found, out, err := excludeLine(file, line)
174	if err != nil {
175		return err
176	}
177	if !found {
178		return service.ErrNoInstalled
179	}
180	if bytes.Equal(bytes.TrimSpace(out), []byte("#!/bin/sh")) {
181		return os.Remove(file)
182	}
183	return ioutil.WriteFile(file, out, 0755)
184}
185
186var tmpl = `#!/bin/sh
187
188name="{{.Name}}"
189exe="{{.Executable}}"
190cmd="$exe{{range .Arguments}} {{.}}{{end}}"
191pid_file="/tmp/$name.pid"
192
193get_pid() {
194	cat "$pid_file"
195}
196
197is_running() {
198	test -f "$pid_file" && ps | grep -q "^ *$(get_pid) "
199}
200
201log() {
202	logger -s -t "${name}.init" "$@"
203}
204
205setup_tz() {
206	tz="$(nvram get time_zone)"
207	tz_dir="/jffs/zoneinfo"
208	tz_file="$tz_dir/$tz"
209	tz_url="https://github.com/nextdns/nextdns/raw/master/router/merlin/tz/$tz"
210	if [ "$(readlink /etc/localtime)" != "$tz_file" ]; then
211		if [ -f "$tz_file" ]; then
212			ln -sf "$tz_file" /etc/localtime
213		else
214			mkdir -p "$tz_dir"
215			if curl -sLo "$tz_file" "$tz_url"; then
216				ln -sf "$tz_file" /etc/localtime
217			fi
218		fi
219	fi
220}
221
222case "$1" in
223	start)
224		if is_running; then
225			log "Already started"
226		else
227			if [ -f /rom/ca-bundle.crt ]; then
228				# Johns fork 39E3j9527 has trust store in non-standard location
229				export SSL_CERT_FILE=/rom/ca-bundle.crt
230			fi
231			setup_tz
232			unset TZ
233			export {{.RunModeEnv}}=1
234			$cmd &
235			echo $! > "$pid_file"
236			if ! is_running; then
237				log "Unable to start"
238				exit 1
239			fi
240		fi
241
242		# Install a symlink of the service into the path if not already present
243		if [ -z "$(which $(basename $exe))" ]; then
244			# /home/$USER is in the path and does not seem to conflict with stuff
245			# like entware.
246			mkdir -p /home/$USER
247			ln -s "$exe" "/home/$USER/$(basename $exe)"
248		fi
249	;;
250	stop)
251		if is_running; then
252			kill $(get_pid)
253			for i in 1 2 3 4 5 6 7 8 9 10; do
254				if ! is_running; then
255					break
256				fi
257				sleep 1
258			done
259			if is_running; then
260				log "Not stopped; may still be shutting down or shutdown may have failed"
261				exit 1
262			else
263				log "Stopped"
264				if [ -f "$pid_file" ]; then
265					rm "$pid_file"
266				fi
267			fi
268		else
269			log "Not running"
270		fi
271	;;
272	restart)
273		$0 stop
274		if is_running; then
275			log "Unable to stop, will not attempt to start"
276			exit 1
277		fi
278		$0 start
279	;;
280	status)
281		if is_running; then
282			log "Running"
283		else
284			log "Stopped"
285			exit 1
286		fi
287	;;
288	*)
289	log "Usage: $0 {start|stop|restart|status}"
290	exit 1
291	;;
292esac
293exit 0
294`
295