Skip to content

Commit de5ee0b

Browse files
committed
refactor + add tests
1 parent 4f6cbbf commit de5ee0b

File tree

12 files changed

+369
-170
lines changed

12 files changed

+369
-170
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# go-remote
1+
# bashRPC
22
Simple HTTP server that executes configured commands remotely.
33

44

command.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"os/exec"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
func runCommand(command string) (out []byte, err error) {
10+
out, err = exec.Command("bash", "-c", command).Output()
11+
12+
if err != nil {
13+
return out, errors.Wrap(err, string(out))
14+
}
15+
16+
return
17+
}

command_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/binarymason/bashRPC/internal/testhelpers"
7+
"github.com/pkg/errors"
8+
)
9+
10+
func TestMultilineCommand(t *testing.T) {
11+
command := `
12+
echo foo
13+
echo bar
14+
echo baz
15+
`
16+
17+
out, err := runCommand(command)
18+
Assert(err, nil, t)
19+
Assert(string(out), "foo\nbar\nbaz\n", t)
20+
}
21+
22+
func TestFailingCommand(t *testing.T) {
23+
out, err := runCommand("echo 'BOOM!' && exit 1")
24+
25+
if err == nil {
26+
t.Error("expected an error but received none")
27+
}
28+
29+
Assert(string(out), "BOOM!\n", t)
30+
expectedErr := errors.Wrap(errors.New("exit status 1"), "BOOM!\n")
31+
Assert(err, expectedErr, t)
32+
}
33+
34+
func TestPipedCommand(t *testing.T) {
35+
command := `echo "it works with pipe" | grep pipe | awk '{ print $1 " " $2 }'`
36+
out, err := runCommand(command)
37+
Assert(err, nil, t)
38+
Assert(string(out), "it works\n", t)
39+
40+
}

config.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"io/ioutil"
6+
"strings"
7+
8+
"gopkg.in/yaml.v2"
9+
)
10+
11+
type config struct {
12+
Port string `yaml:"port"`
13+
Secret string `yaml:"secret"`
14+
Whitelisted []string `yaml:"whitelisted_clients"`
15+
Routes []route `yaml:"routes"`
16+
}
17+
18+
type route struct {
19+
Path string `yaml:"path"`
20+
Cmd string `yaml:"cmd"`
21+
}
22+
23+
func loadConfig(p string) (config, error) {
24+
cfg := config{}
25+
data, err := ioutil.ReadFile(p)
26+
if err != nil {
27+
return cfg, err
28+
}
29+
30+
err = yaml.Unmarshal([]byte(data), &cfg)
31+
return cfg, err
32+
}
33+
34+
func validateConfig(cfg config) error {
35+
var issues []string
36+
37+
if cfg.Port == "" {
38+
issues = append(issues, "port is missing")
39+
}
40+
41+
if cfg.Secret == "" {
42+
issues = append(issues, "secret is missing")
43+
}
44+
45+
if len(cfg.Whitelisted) == 0 {
46+
issues = append(issues, "no whitelisted clients are specified")
47+
}
48+
49+
if len(issues) > 0 {
50+
return errors.New("config validation errors: " + strings.Join(issues, ", "))
51+
}
52+
53+
return nil
54+
}

main_test.go renamed to config_test.go

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,13 @@
11
package main
22

33
import (
4-
"fmt"
54
"testing"
6-
)
7-
8-
func Given(s string) {
9-
fmt.Println("Given", s)
10-
}
11-
12-
func When(s string) {
13-
fmt.Println(" When", s)
14-
}
15-
16-
func Then(s string) {
17-
fmt.Println(" Then", s)
18-
}
195

20-
func And(s string) {
21-
fmt.Println(" And", s)
22-
}
23-
24-
func Assert(a, x interface{}, t *testing.T) {
25-
26-
a = fmt.Sprintf("%v", a)
27-
x = fmt.Sprintf("%v", x)
28-
if a != x {
29-
t.Errorf("Expected %s, but got: %s", x, a)
30-
}
31-
}
6+
. "github.com/binarymason/bashRPC/internal/testhelpers"
7+
)
328

339
func TestLoad(t *testing.T) {
34-
cfg, _ := loadConfig("./testdata/simple_config.yml")
10+
cfg, _ := loadConfig("./test/data/simple_config.yml")
3511
expectedRoutes := []route{
3612
route{Path: "/foo", Cmd: "echo foo"},
3713
route{Path: "/bar", Cmd: "echo bar"},

internal/testhelpers/testhelpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package testhelpers
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func Given(s string) {
9+
fmt.Println("Given", s)
10+
}
11+
12+
func When(s string) {
13+
fmt.Println(" When", s)
14+
}
15+
16+
func Then(s string) {
17+
fmt.Println(" Then", s)
18+
}
19+
20+
func And(s string) {
21+
fmt.Println(" And", s)
22+
}
23+
24+
func Assert(a, x interface{}, t *testing.T) {
25+
26+
a = fmt.Sprintf("%v", a)
27+
x = fmt.Sprintf("%v", x)
28+
if a != x {
29+
t.Errorf("Expected %s, but got: %s", x, a)
30+
}
31+
}

main.go

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
11
package main
22

33
import (
4-
"errors"
54
"flag"
65
"fmt"
7-
"io/ioutil"
86
"log"
9-
"net/http"
107
"os"
11-
"os/exec"
12-
"strings"
13-
14-
"gopkg.in/yaml.v2"
158
)
169

17-
type config struct {
18-
Port string `yaml:"port"`
19-
Secret string `yaml:"secret"`
20-
Whitelisted []string `yaml:"whitelisted_clients"`
21-
Routes []route `yaml:"routes"`
22-
}
23-
24-
type route struct {
25-
Path string `yaml:"path"`
26-
Cmd string `yaml:"cmd"`
27-
}
28-
29-
type router struct {
30-
config config
31-
}
32-
3310
var configPath string
3411

3512
func init() {
@@ -38,9 +15,6 @@ func init() {
3815
configPath = *c
3916
}
4017

41-
// TODO:
42-
// * check authorized host
43-
// * check authorized auth
4418
func main() {
4519
if configPath == "" {
4620
fmt.Println("config file argument is required")
@@ -56,119 +30,3 @@ func main() {
5630

5731
rtr.listen()
5832
}
59-
60-
func newRouter(p string) (router, error) {
61-
rtr := router{}
62-
cfg, err := loadConfig(p)
63-
64-
if err != nil {
65-
return rtr, err
66-
}
67-
68-
if err := validateConfig(cfg); err != nil {
69-
return rtr, err
70-
}
71-
72-
rtr.config = cfg
73-
74-
return rtr, nil
75-
}
76-
77-
func loadConfig(p string) (config, error) {
78-
cfg := config{}
79-
data, err := ioutil.ReadFile(p)
80-
if err != nil {
81-
return cfg, err
82-
}
83-
84-
err = yaml.Unmarshal([]byte(data), &cfg)
85-
return cfg, err
86-
}
87-
88-
func validateConfig(cfg config) error {
89-
var issues []string
90-
91-
if cfg.Port == "" {
92-
issues = append(issues, "port is missing")
93-
}
94-
95-
if cfg.Secret == "" {
96-
issues = append(issues, "secret is missing")
97-
}
98-
99-
if len(cfg.Whitelisted) == 0 {
100-
issues = append(issues, "no whitelisted clients are specified")
101-
}
102-
103-
if len(issues) > 0 {
104-
return errors.New("config validation errors: " + strings.Join(issues, ", "))
105-
}
106-
107-
return nil
108-
}
109-
110-
func (rtr *router) listen() {
111-
http.HandleFunc("/", rtr.handler)
112-
113-
fmt.Println("listening on port", rtr.config.Port)
114-
http.ListenAndServe(":"+rtr.config.Port, nil)
115-
116-
}
117-
118-
func (rtr *router) handler(w http.ResponseWriter, r *http.Request) {
119-
if !rtr.authorizedRequest(r) {
120-
http.Error(w, "Unauthorized", http.StatusUnauthorized)
121-
return
122-
}
123-
124-
path := r.URL.Path
125-
126-
route, err := rtr.routeForPath(path)
127-
128-
if err != nil {
129-
http.Error(w, "invalid route", http.StatusNotFound)
130-
return
131-
}
132-
133-
command, args := parseCommand(route.Cmd)
134-
135-
out, err := exec.Command(command, args...).Output()
136-
if err != nil {
137-
http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError)
138-
return
139-
}
140-
141-
w.Write(out)
142-
}
143-
144-
func (rtr *router) authorizedRequest(r *http.Request) bool {
145-
ip := strings.Split(r.RemoteAddr, ":")[0] // remove port
146-
auth := r.Header.Get("Authorization")
147-
148-
return validIP(ip, rtr.config.Whitelisted) && (auth == rtr.config.Secret)
149-
}
150-
151-
func validIP(ip string, whitelisted []string) bool {
152-
for _, w := range whitelisted {
153-
if ip == w {
154-
return true
155-
}
156-
}
157-
158-
return false
159-
}
160-
161-
func (rtr *router) routeForPath(path string) (r route, err error) {
162-
for _, route := range rtr.config.Routes {
163-
if route.Path == path {
164-
return route, nil
165-
}
166-
}
167-
168-
return r, errors.New("Route not found: " + path)
169-
}
170-
171-
func parseCommand(s string) (c string, args []string) {
172-
command := strings.Split(s, " ")
173-
return command[0], command[1:]
174-
}

0 commit comments

Comments
 (0)