Skip to content

Commit 1d7ad84

Browse files
committed
Initial commit
0 parents  commit 1d7ad84

File tree

8 files changed

+703
-0
lines changed

8 files changed

+703
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Favyen Bastani
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Launch Gogs on LunaNode
2+
-----------------------
3+
4+
To get started with Gogs on LunaNode, just head to https://launchgogs.lunanode.com
5+
6+
The code for the launcher is in this repository. The launcher will setup a Gogs
7+
installation with HTTPS. The git SSH port will be on non-standard 10022.

main.go

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/sha512"
6+
"crypto/hmac"
7+
"encoding/hex"
8+
"encoding/json"
9+
"fmt"
10+
"io/ioutil"
11+
"log"
12+
"net/http"
13+
"net/url"
14+
"strconv"
15+
"strings"
16+
"time"
17+
)
18+
19+
const IMAGE_ID = 121428
20+
21+
type ErrorResponse struct {
22+
Error string `json:"error"`
23+
}
24+
25+
type GetIPResponse struct {
26+
IP string `json:"ip"`
27+
}
28+
29+
type NullResponse struct {}
30+
31+
type LunaSshkeyAdd struct {
32+
KeyID string `json:"key_id"`
33+
}
34+
35+
type LunaScriptCreate struct {
36+
ScriptID string `json:"script_id"`
37+
}
38+
39+
type LunaNetworkList struct {
40+
Networks []struct {
41+
NetID string `json:"net_id"`
42+
} `json:"networks"`
43+
}
44+
45+
type LunaFloatingList struct {
46+
IPs []struct {
47+
IP string `json:"ip"`
48+
AttachedType string `json:"attached_type"`
49+
Region string `json":region"`
50+
} `json:"ips"`
51+
}
52+
53+
type LunaVmCreate struct {
54+
VmID string `json:"vm_id"`
55+
}
56+
57+
type LunaDynList struct {
58+
Dyns map[string]struct{
59+
ID string `json:"id"`
60+
Name string `json:"name"`
61+
IP string `json:"ip"`
62+
} `json:"dyns"`
63+
}
64+
65+
func main() {
66+
scriptBytes, err := ioutil.ReadFile("run.sh")
67+
if err != nil {
68+
panic(err)
69+
}
70+
script := string(scriptBytes)
71+
72+
fileServer := http.FileServer(http.Dir("static/"))
73+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
74+
if r.URL.Path == "/" {
75+
w.Header().Set("Cache-Control", "no-cache")
76+
}
77+
fileServer.ServeHTTP(w, r)
78+
})
79+
http.HandleFunc("/getip", func(w http.ResponseWriter, r *http.Request) {
80+
r.ParseForm()
81+
apiID := r.PostForm.Get("api_id")
82+
apiKey := r.PostForm.Get("api_key")
83+
ip, err := getFreeFloatingIP(apiID, apiKey, "toronto")
84+
if err != nil {
85+
errorResponse(w, r, err.Error())
86+
return
87+
}
88+
if ip == "" {
89+
err := request(apiID, apiKey, "floating", "add", map[string]string{
90+
"region": "toronto",
91+
}, nil)
92+
if err != nil {
93+
errorResponse(w, r, err.Error())
94+
return
95+
}
96+
ip, _ = getFreeFloatingIP(apiID, apiKey, "toronto")
97+
if ip == "" {
98+
errorResponse(w, r, "failed to get an external IP")
99+
return
100+
}
101+
}
102+
jsonResponse(w, GetIPResponse{ip})
103+
})
104+
105+
http.HandleFunc("/launch", func(w http.ResponseWriter, r *http.Request) {
106+
r.ParseForm()
107+
apiID := r.PostForm.Get("api_id")
108+
apiKey := r.PostForm.Get("api_key")
109+
ip := r.PostForm.Get("ip")
110+
hostname := r.PostForm.Get("hostname")
111+
sshKey := r.PostForm.Get("sshkey")
112+
plan := r.PostForm.Get("plan")
113+
114+
remoteIP := r.RemoteAddr
115+
116+
myscript := script
117+
myscript = strings.Replace(myscript, "[HOSTNAME]", hostname, -1)
118+
119+
var cleanupFuncs []func()
120+
cleanup := func() {
121+
for _, f := range cleanupFuncs {
122+
f()
123+
}
124+
}
125+
126+
// create DNS if desired
127+
if strings.HasSuffix(hostname, ".lndyn.com") && strings.HasPrefix(hostname, "gogs") && len(hostname) == 21 {
128+
err := request(apiID, apiKey, "dns", "dyn-add", map[string]string{
129+
"name": strings.Split(hostname, ".")[0],
130+
"ip": ip,
131+
}, nil)
132+
if err != nil {
133+
cleanup()
134+
errorResponse(w, r, err.Error())
135+
return
136+
}
137+
cleanupFuncs = append(cleanupFuncs, func() {
138+
var response LunaDynList
139+
if err := request(apiID, apiKey, "dns", "dyn-list", nil, &response); err != nil {
140+
log.Printf("warning: error listing dyns for cleanup: %v", err)
141+
return
142+
}
143+
for _, dyn := range response.Dyns {
144+
if dyn.Name == strings.Split(hostname, ".")[0] || dyn.IP == ip {
145+
request(apiID, apiKey, "dns", "dyn-remove", map[string]string{"dyn_id": dyn.ID}, nil)
146+
}
147+
}
148+
})
149+
}
150+
151+
params := map[string]string{
152+
"region": "toronto",
153+
"plan_id": plan,
154+
"image_id": strconv.Itoa(IMAGE_ID),
155+
"ip": ip,
156+
"hostname": hostname,
157+
}
158+
159+
log.Printf("[%s] creating vm", remoteIP)
160+
161+
// add ssh key, if set
162+
if sshKey != "" {
163+
var keyResponse LunaSshkeyAdd
164+
err := request(apiID, apiKey, "sshkey", "add", map[string]string{
165+
"label": "tmp",
166+
"sshkey": sshKey,
167+
}, &keyResponse)
168+
if err != nil {
169+
cleanup()
170+
errorResponse(w, r, "error adding SSH key: " + err.Error())
171+
return
172+
}
173+
params["key_id"] = keyResponse.KeyID
174+
defer func() {
175+
request(apiID, apiKey, "sshkey", "remove", map[string]string{
176+
"key_id": keyResponse.KeyID,
177+
}, nil)
178+
}()
179+
} else {
180+
params["set_password"] = "yes"
181+
}
182+
183+
// add startup script
184+
var scriptResponse LunaScriptCreate
185+
err = request(apiID, apiKey, "script", "create", map[string]string{
186+
"name": "tmp-gogs",
187+
"content": myscript,
188+
}, &scriptResponse)
189+
if err != nil {
190+
cleanup()
191+
errorResponse(w, r, "error creating startup script: " + err.Error())
192+
return
193+
}
194+
params["scripts"] = scriptResponse.ScriptID
195+
defer func() {
196+
request(apiID, apiKey, "script", "delete", map[string]string{
197+
"script_id": scriptResponse.ScriptID,
198+
}, nil)
199+
}()
200+
201+
// should we set network?
202+
var networkResponse LunaNetworkList
203+
request(apiID, apiKey, "network", "list", map[string]string{
204+
"region": "toronto",
205+
}, &networkResponse)
206+
if len(networkResponse.Networks) >= 1 {
207+
params["net_id"] = networkResponse.Networks[0].NetID
208+
}
209+
210+
// create vm
211+
var vmResponse LunaVmCreate
212+
err = request(apiID, apiKey, "vm", "create", params, &vmResponse)
213+
if err != nil {
214+
cleanup()
215+
errorResponse(w, r, "error creating VM: " + err.Error())
216+
return
217+
}
218+
219+
jsonResponse(w, NullResponse{})
220+
})
221+
log.Fatal(http.ListenAndServe(":8080", nil))
222+
}
223+
224+
func errorResponse(w http.ResponseWriter, r *http.Request, msg string) {
225+
log.Printf("[%s] error: %s", r.RemoteAddr, msg)
226+
jsonResponse(w, ErrorResponse{msg})
227+
}
228+
229+
func jsonResponse(w http.ResponseWriter, x interface{}) {
230+
bytes, err := json.Marshal(x)
231+
if err != nil {
232+
panic(err)
233+
}
234+
w.Header().Set("Content-Type", "application/json")
235+
w.Write(bytes)
236+
}
237+
238+
const LNDYNAMIC_API_URL = "https://dynamic.lunanode.com/api/{CATEGORY}/{ACTION}/"
239+
240+
func request(apiID string, apiKey string, category string, action string, params map[string]string, target interface{}) error {
241+
if len(apiID) != 16 {
242+
return fmt.Errorf("API ID should be 16 characters")
243+
} else if len(apiKey) != 128 {
244+
return fmt.Errorf("API key should be 128 characters")
245+
}
246+
247+
// construct URL
248+
targetUrl := LNDYNAMIC_API_URL
249+
targetUrl = strings.Replace(targetUrl, "{CATEGORY}", category, -1)
250+
targetUrl = strings.Replace(targetUrl, "{ACTION}", action, -1)
251+
252+
// get raw parameters string
253+
if params == nil {
254+
params = make(map[string]string)
255+
}
256+
params["api_id"] = apiID
257+
params["api_partialkey"] = apiKey[:64]
258+
rawParams, err := json.Marshal(params)
259+
if err != nil {
260+
return err
261+
}
262+
263+
// HMAC signature with nonce
264+
nonce := fmt.Sprintf("%d", time.Now().Unix())
265+
handler := fmt.Sprintf("%s/%s/", category, action)
266+
hashTarget := fmt.Sprintf("%s|%s|%s", handler, string(rawParams), nonce)
267+
hasher := hmac.New(sha512.New, []byte(apiKey))
268+
_, err = hasher.Write([]byte(hashTarget))
269+
if err != nil {
270+
return err
271+
}
272+
signature := hex.EncodeToString(hasher.Sum(nil))
273+
274+
// perform request
275+
values := url.Values{}
276+
values.Set("handler", handler)
277+
values.Set("req", string(rawParams))
278+
values.Set("signature", signature)
279+
values.Set("nonce", nonce)
280+
byteBuffer := new(bytes.Buffer)
281+
byteBuffer.Write([]byte(values.Encode()))
282+
response, err := http.Post(targetUrl, "application/x-www-form-urlencoded", byteBuffer)
283+
if err != nil {
284+
return err
285+
}
286+
responseBytes, err := ioutil.ReadAll(response.Body)
287+
if err != nil {
288+
return err
289+
}
290+
response.Body.Close()
291+
292+
// decode JSON
293+
// we first decode into generic response for error checking; then into specific response to return
294+
var genericResponse struct {
295+
Success string `json:"success"`
296+
Error string `json:"error"`
297+
}
298+
299+
err = json.Unmarshal(responseBytes, &genericResponse)
300+
301+
if err != nil {
302+
return err
303+
} else if genericResponse.Success != "yes" {
304+
if genericResponse.Error != "" {
305+
return fmt.Errorf(genericResponse.Error)
306+
} else {
307+
return fmt.Errorf("backend call failed for unknown reason")
308+
}
309+
}
310+
311+
if target != nil {
312+
err = json.Unmarshal(responseBytes, target)
313+
if err != nil {
314+
return err
315+
}
316+
}
317+
318+
return nil
319+
}
320+
321+
func getFreeFloatingIP(apiID string, apiKey string, region string) (string, error) {
322+
var response LunaFloatingList
323+
if err := request(apiID, apiKey, "floating", "list", nil, &response); err != nil {
324+
return "", err
325+
}
326+
for _, ip := range response.IPs {
327+
if ip.AttachedType == "unattached" && ip.Region == region {
328+
return ip.IP, nil
329+
}
330+
}
331+
return "", nil
332+
}

0 commit comments

Comments
 (0)