-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
156 lines (133 loc) · 4.04 KB
/
main.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
package main
import (
"log"
"net/http"
"os"
"os/exec"
"fmt"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"strings"
"io/ioutil"
)
func main() {
// start logging
logFile := os.Getenv("LOG_FILE")
// log to file
f, err := os.OpenFile(logFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
log.Println("------")
log.Println("Starting simple CI service")
// required env variables
// TCP PORT number
// eg.: ':30000'
port := os.Getenv("TCP_PORT")
// root apps directory
appsRoot := os.Getenv("APPS_ROOT")
// prefix for Systemd services
// eg.: 'node-' for 'node-{app}' services
servicesPrefix := os.Getenv("SERVICES_PREFIX")
// commands to execute deployment
// update repository source from remote
cmdGitFetch := "git fetch --all"
gitReset := "git reset --hard origin/"
// update NPM dependencies
cmdNpm := "npm update"
// setup HTTP client
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// app name from query string
_app, ok := r.URL.Query()["AppName"]
if !ok || len(_app) < 1 {
clientError(w, []byte("No `AppName` query param!\n"))
return
}
// git branch to sync
// eg.: 'production'
_branch, ok := r.URL.Query()["GitBranch"]
if !ok || len(_branch) < 1 {
clientError(w, []byte("No `GitBranch` query param!\n"))
return
}
// secret to validate GitHub hook
_secret, ok := r.URL.Query()["Secret"]
if !ok || len(_secret) < 1 {
clientError(w, []byte("No `Secret` query param!\n"))
return
}
app := _app[0]
branch := _branch[0]
secret := _secret[0]
dir := fmt.Sprintf("%s%s", appsRoot, app)
// validate signature header and body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
clientError(w, []byte("Cannot handle the request body!\n"))
return
}
signature := r.Header.Get("x-hub-signature")
if len(signature) == 0 {
clientError(w, []byte("No signature header!\n"))
return
}
log.Println("----- Request -----")
log.Println(app)
// handle hash validation
if !verifySignature([]byte(secret), signature, body) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized!\n"))
return
}
// check if directory exists first
if _, err := os.Stat(dir); !os.IsNotExist(err) {
// git reset command with received branch
cmdGitPull := fmt.Sprintf("%s%s", gitReset, branch)
// command to restart app Systemd service
cmdSystemd := fmt.Sprintf("systemctl restart %s%s", servicesPrefix, app)
// merge all commands
shCommand := fmt.Sprintf("%s && %s && %s && %s", cmdGitFetch, cmdGitPull, cmdNpm, cmdSystemd)
cmd := exec.Command("/bin/sh", "-c", shCommand)
// move to app directory
cmd.Dir = dir
// execute commands without waiting to complete
cmd.Start()
log.Println("==> Deploy")
log.Println(dir)
log.Println(shCommand)
w.WriteHeader(http.StatusOK)
w.Write([]byte(dir))
} else {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Skip directory not found!\n"))
}
})
log.Println("Listening...")
log.Println(port)
log.Fatal(http.ListenAndServe(port, nil))
}
func clientError(w http.ResponseWriter, msg []byte) {
// 400 response
w.WriteHeader(http.StatusBadRequest)
w.Write(msg)
}
// Reference:
// https://gist.github.com/rjz/b51dc03061dbcff1c521
func signBody(secret, body []byte) []byte {
computed := hmac.New(sha1.New, secret)
computed.Write(body)
return []byte(computed.Sum(nil))
}
func verifySignature(secret []byte, signature string, body []byte) bool {
const signaturePrefix = "sha1="
const signatureLength = 45 // len(SignaturePrefix) + len(hex(sha1))
if len(signature) != signatureLength || !strings.HasPrefix(signature, signaturePrefix) {
return false
}
actual := make([]byte, 20)
hex.Decode(actual, []byte(signature[5:]))
return hmac.Equal(signBody(secret, body), actual)
}