-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathserver.go
202 lines (166 loc) · 4.25 KB
/
server.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
package hookah
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"sync"
"time"
multierror "github.com/hashicorp/go-multierror"
)
var ErrPathIsNotDir = errors.New("path is not a dir")
var validGhEvent = regexp.MustCompile(`^[a-z\d_]{1,30}$`)
// Logger handles Printf
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}
// HookServer implements net/http.Handler
type HookServer struct {
RootDir string
Timeout time.Duration
ErrorLog Logger
InfoLog Logger
sync.Mutex
}
// ServerOption sets an option of the HookServer
type ServerOption func(*HookServer) error
// NewHookServer instantiates a new HookServer with some basic validation
// on the root directory
func NewHookServer(rootDir string, options ...ServerOption) (*HookServer, error) {
absRootDir, err := filepath.Abs(rootDir)
if err != nil {
return nil, fmt.Errorf("failed converting server-root '%s' to an absolute path: %w", rootDir, err)
}
f, err := os.Open(absRootDir)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, ErrPathIsNotDir
}
server := &HookServer{
RootDir: absRootDir,
}
var result *multierror.Error
for _, option := range options {
err := option(server)
result = multierror.Append(result, err)
}
return server, result.ErrorOrNil()
}
// ServerExecTimeout configures the HookServer per-script execution timeout
func ServerExecTimeout(timeout time.Duration) ServerOption {
return func(h *HookServer) error {
h.Timeout = timeout
return nil
}
}
// ServerErrorLog configures the HookServer error logger
func ServerErrorLog(log Logger) ServerOption {
return func(h *HookServer) error {
h.ErrorLog = log
return nil
}
}
// ServerInfoLog configures the HookServer info logger
func ServerInfoLog(log Logger) ServerOption {
return func(h *HookServer) error {
h.InfoLog = log
return nil
}
}
func (h *HookServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ghEvent := r.Header.Get("X-Github-Event")
if !validGhEvent.MatchString(ghEvent) {
http.Error(w, "Request requires valid X-Github-Event", http.StatusBadRequest)
return
}
if ghEvent == "ping" {
fmt.Fprintln(w, "pong")
return
}
ghDelivery := r.Header.Get("X-GitHub-Delivery")
if ghDelivery == "" {
http.Error(w, "Request requires valid X-GitHub-Delivery", http.StatusBadRequest)
return
}
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(ghDelivery, err)
return
}
buff := bytes.NewReader(b)
basicHook := &HookJSON{}
decoder := json.NewDecoder(buff)
err = decoder.Decode(basicHook)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Println(ghDelivery, err)
return
}
login := basicHook.Repository.Owner.GetLogin()
repo := basicHook.Repository.Name
if repo == "" || login == "" {
msg := "Unexpected JSON HTTP Body"
http.Error(w, msg, http.StatusBadRequest)
log.Println(ghDelivery, msg)
return
}
action := basicHook.Action
fmt.Fprintf(w, "%s/%s", login, repo)
hook := HookExec{
RootDir: h.RootDir,
Data: buff,
InfoLog: h.InfoLog,
}
go func() {
h.Lock()
defer h.Unlock()
err := hook.Exec(login, repo, ghEvent, action, h.Timeout,
"GITHUB_DELIVERY="+ghDelivery,
"GITHUB_LOGIN="+login,
"GITHUB_REPO="+repo,
"GITHUB_EVENT="+ghEvent,
"GITHUB_ACTION="+action,
"HOOKAH_SERVER_ROOT="+h.RootDir,
)
if err != nil && h.ErrorLog != nil {
h.ErrorLog.Printf("%s - %s/%s:%s - '%s'", ghDelivery, login, repo, ghEvent, err)
}
}()
}
// HookUserJSON exists because some hooks use Login, some use Name
// - it's horribly inconsistent and a bad flaw on GitHubs part
type HookUserJSON struct {
Login string `json:"login"`
Name string `json:"name"`
}
// GetLogin is used to get the login from the data github decided to pass today
func (h *HookUserJSON) GetLogin() string {
if h.Login != "" {
return h.Login
}
return h.Name
}
// HookJSON represents the minimum body we need to parse
type HookJSON struct {
Action string `json:"action,omitempty"`
Repository struct {
Name string `json:"name"`
Owner HookUserJSON `json:"owner"`
} `json:"repository"`
Sender HookUserJSON `json:"sender"`
}