· 6 years ago · Jul 22, 2019, 04:12 PM
1package main
2
3import (
4 "bytes"
5 "crypto/tls"
6 "crypto/x509"
7 "encoding/json"
8 "fmt"
9 "github.com/go-pg/pg"
10 "github.com/gorilla/mux"
11 _ "github.com/lib/pq"
12 "io/ioutil"
13 "log"
14 "net/http"
15 "net/url"
16 "strings"
17)
18
19type ApiFacade struct {
20
21}
22
23/**
24 Get right TLS config for secure connections
25 */
26func getTlsConfig() (*tls.Config) {
27 cert, err := tls.LoadX509KeyPair("./maxroach.crt", "./maxroach.key")
28 if err != nil {
29 log.Fatal("failed to load client certificate: %v", err)
30 }
31
32 CAFile := "ca.crt"
33
34 CACert, err := ioutil.ReadFile(CAFile)
35 if err != nil {
36 log.Fatal("failed to load server certificate: %v", err)
37 }
38
39 CACertPool := x509.NewCertPool()
40 CACertPool.AppendCertsFromPEM(CACert)
41
42 tlsConfig := &tls.Config{
43 Certificates: []tls.Certificate{cert},
44 RootCAs: CACertPool,
45 InsecureSkipVerify: true,
46 ServerName: "localhost",
47 }
48
49 return tlsConfig
50}
51
52// Connect to CockroachDB
53func connectToDb() (*pg.DB) {
54 db := pg.Connect(&pg.Options{
55 User: "maxroach",
56 Password: "123456",
57 Addr: "localhost:26257",
58 Database: "dh_shortner",
59 TLSConfig: getTlsConfig(),
60 })
61
62 createTables(db)
63
64 return db
65}
66
67// Create tables for shortner
68func createTables(db *pg.DB) {
69 qs := []string{
70 "CREATE SEQUENCE IF NOT EXISTS users_id_seq",
71 "CREATE SEQUENCE IF NOT EXISTS links_id_seq",
72 "CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, dh_user_id INT, info TEXT)",
73 "ALTER TABLE users ALTER COLUMN id SET DEFAULT NEXTVAL('users_id_seq')",
74 "CREATE TABLE IF NOT EXISTS links (id INT PRIMARY KEY, dh_user_id INT, original_url TEXT, shortened_url TEXT, seed TEXT, attributes TEXT)",
75 "ALTER TABLE links ALTER COLUMN id SET DEFAULT NEXTVAL('links_id_seq')",
76 }
77
78 for _, q := range qs {
79 _, err := db.Exec(q)
80 if err != nil {
81 panic(err)
82 }
83 }
84}
85
86/**
87 Net query with all headers
88 */
89func (a *ApiFacade) netQuery(requestKey string, jsonIn []byte) ([]byte) {
90 apiQueriesMap := map[string]map[string]string{
91 "create" : {
92 "url" : "/api/v1/protected/create",
93 "method" : "POST",
94 },
95 "display" : {
96 "url" : "/api/v1/displayURL",
97 "method" : "GET",
98 },
99 }
100
101 fmt.Println(apiQueriesMap[requestKey]["method"] + " -> " + apiQueriesMap[requestKey]["url"])
102
103 client := &http.Client{}
104
105 req, err := http.NewRequest(apiQueriesMap[requestKey]["method"], "http://localhost:8082" + apiQueriesMap[requestKey]["url"], bytes.NewBuffer(jsonIn))
106
107 req.Header.Add("Content-Type", "application/json")
108 req.Header.Add("X-Goog-Authenticated-User-ID", "proxy-authed")
109 req.Header.Add("X-Goog-Authenticated-User-Email", "dummy@host.com")
110
111 resp, err := client.Do(req)
112
113 defer resp.Body.Close()
114
115 if err != nil {
116 panic(err)
117 }
118
119 body, err := ioutil.ReadAll(resp.Body)
120 if err != nil {
121 log.Fatalln(err)
122 }
123
124 return body
125}
126
127/** --------------- API -------------- **/
128
129type ShortenedLink struct {
130 Id string `json:"id"` // Generated ID
131 Shortcode string `json:"shortcode"` // the URL fragment, dh-short.url/<shortcode>
132}
133
134type Status struct {
135 IsExists bool `json:"exists"`
136}
137
138/**
139 Answer for getting shortened links
140 */
141type AnswerShortLink struct {
142 Status Status `json:"status"`
143 ShortenedLink ShortenedLink `json:"shortenedLink"`
144}
145
146type GetUserShortenLinkAttributesInParams struct {
147 ContactId string `json:"contactId"`
148 MessageId string `json:"messageId"`
149}
150
151type GetUserShortenLinkInParams struct {
152 Seed string `json:"seed"`
153 Attributes GetUserShortenLinkAttributesInParams `json:"attributes"`
154}
155
156/**
157 Short user link and put them into DB
158 */
159func (a *ApiFacade) shortUserLink(w http.ResponseWriter, r *http.Request) {
160 fmt.Println("getUserShortenLink");
161
162 decoder := json.NewDecoder(r.Body)
163 var jsonIn GetUserShortenLinkInParams
164 err := decoder.Decode(&jsonIn)
165 if err != nil {
166 panic(err)
167 }
168
169 if jsonIn.Seed == "" {
170 log.Fatal("Seed param is missing")
171 }
172
173 params := mux.Vars(r)
174 db := connectToDb();
175 dbLink := findLinkByUserAndLink(params["userId"], params["linkId"], db)
176
177 if dbLink.Id == -1 {
178 w.WriteHeader(http.StatusNotFound)
179 log.Println("404 - Resource [userId = " + params["userId"] + ", linkId = " + params["linkId"] + "] not found")
180 w.Write([]byte("404 - Resource not found"))
181 } else {
182
183 if dbLink.Seed == jsonIn.Seed {
184 w.WriteHeader(http.StatusBadRequest)
185 log.Println("403 - Forbidden. This seed is already used by user and his link")
186 json.NewEncoder(w).Encode(map[string]string{
187 "code" : "403",
188 "status" : "This seed is already used by user and his link",
189 })
190 } else {
191
192
193 apiParams, err := json.Marshal(map[string]string{
194 "URL" : dbLink.OriginalUrl,
195 })
196
197 if err != nil {
198 // @TODO Catch error
199 }
200
201 result := a.netQuery("create", apiParams)
202
203 var apiAnswer CreateUrlApiAnswer
204 if err := json.Unmarshal(result, &apiAnswer); err != nil {
205 // panic(err)
206 }
207
208 attributes, attributesErr := json.Marshal(jsonIn.Attributes);
209 if attributesErr != nil {
210 // @TODO Here will be catching of error...
211 }
212
213 u, err := url.Parse(apiAnswer.Url)
214 path := u.EscapedPath()[1:len(u.EscapedPath())]
215
216 if dbLink.Seed != "" { // @TODO Refactor this..
217 cloneDbLink(*dbLink, db)
218
219 }
220
221 putShortenLinkIntoDb(dbLink.Id, path, jsonIn.Seed, string(attributes), db)
222
223 w.Header().Set("Content-Type", "application/json")
224
225 var shortenedLink AnswerShortLink
226
227 shortenedLink.Status.IsExists = false
228 shortenedLink.ShortenedLink.Id = params["linkId"]
229 shortenedLink.ShortenedLink.Shortcode = path
230
231 json.NewEncoder(w).Encode(shortenedLink)
232 }
233 }
234
235 defer r.Body.Close()
236}
237
238/**
239 Target link type
240 */
241type TargetLink struct {
242 Id int `json:"id"`
243}
244
245/**
246 Answer in form of target link
247 */
248type AnswerTargetLink struct {
249 TargetLink TargetLink `json:"targetLink"`
250}
251
252type RegisterUserUrlInParams struct {
253 Url string `json:"url"` // URL that comes in
254}
255
256type CreateUrlApiAnswer struct {
257 DeletionUrl string `json:"DeletionURL"`
258 Expiration string `json:"Expiration"`
259 Id string `json:"ID"`
260 Password string `json:"Password"`
261 Url string `json:"URL"`
262}
263
264type DBLink struct {
265 OriginalUrl string
266 DhUserId string
267 Id int
268}
269
270/**
271 ORM Models
272 */
273type User struct{
274 tableName struct{} `sql:"users"`
275
276 Id int `sql:"id,pk"`
277 DhUserId string `sql:"dh_user_id"`
278 Info string `sql:"info"`
279}
280
281type Link struct{
282 tableName struct{} `sql:"links"`
283
284 Id int `sql:"id,pk"`
285 DhUserId string `sql:"dh_user_id"`
286 OriginalUrl string `sql:"original_url"`
287 ShortenedUrl string `sql:"shortened_url"`
288 Seed string `sql:"seed"`
289 Attributes string `sql:"attributes"`
290}
291
292/**
293 Put link into database
294 */
295func putUserLinkIntoDB(userId string, linkId string, db *pg.DB) (*Link) {
296 // Put user...
297 errInsertUser := db.Insert(&User{
298 DhUserId: userId,
299 Info: "",
300 })
301
302 if errInsertUser != nil {
303 log.Fatal(errInsertUser)
304 }
305
306 newLink := &Link{
307 DhUserId: userId,
308 OriginalUrl: linkId,
309 ShortenedUrl: "",
310 }
311
312 // Put link...
313 errInsertLink := db.Insert(newLink)
314
315 if errInsertLink != nil {
316 log.Fatal(errInsertLink)
317 }
318
319 return newLink;
320}
321
322/**
323 Return link from database
324 */
325func findLinkByUserAndLink(userId string, linkId string, db *pg.DB) (*Link) {
326 link := &Link{
327 Id: -1,
328 DhUserId: "",
329 OriginalUrl: "",
330 ShortenedUrl: "",
331 Seed : "",
332 }
333
334 err := db.Model(link).
335 Where("dh_user_id = ?", userId).
336 Where("id = ?", linkId).
337 Select()
338
339 if err != nil {
340 log.Println(err)
341 }
342
343 return link
344}
345
346/**
347 Put shorten link into DB
348 */
349func putShortenLinkIntoDb(dbLinkId int, shortenLink string, seed string, attributes string, db *pg.DB) (interface{}, interface{}) {
350 res, err := db.Model(&Link{}).
351 Set("shortened_url = ?", shortenLink).
352 Set("seed = ?", seed).
353 Set("attributes = ?", attributes).
354 Where("id = ?", dbLinkId).
355 Update()
356
357 if err != nil {
358 log.Fatal(err)
359 }
360
361 return dbLinkId, res
362}
363
364/**
365 Clone linkin database
366 */
367func cloneDbLink(link Link, db *pg.DB) (*Link) {
368 err := db.Model(&link).
369 Where("id = ?", link.Id).
370 Select()
371
372 if err != nil {
373 log.Fatal(err)
374 }
375
376 clonedLink := &Link{
377 DhUserId: link.DhUserId,
378 OriginalUrl: link.OriginalUrl,
379 ShortenedUrl: link.ShortenedUrl,
380 Attributes: link.Attributes,
381 Seed: "",
382 }
383
384 errInsert := db.Insert(clonedLink)
385 if errInsert != nil {
386 log.Fatal(errInsert)
387 }
388
389 return clonedLink
390}
391
392/**
393 Get link from DB by shortSeedcode
394 */
395func findLinkByShortcode(shortcode string, db *pg.DB) (*Link) {
396 link := &Link{
397 Id: -1,
398 DhUserId: "",
399 OriginalUrl: "",
400 ShortenedUrl: "",
401 Seed : "",
402 }
403
404 err := db.Model(link).
405 Where("shortened_url = ?", shortcode).
406 Select()
407
408 if err != nil {
409 log.Println(err)
410 }
411
412 return link
413}
414
415/**
416 Register user url
417 */
418func (a *ApiFacade) registerUserUrl(w http.ResponseWriter, r *http.Request) {
419 fmt.Println("registerUserUrl")
420
421 decoder := json.NewDecoder(r.Body)
422 jsonIn := RegisterUserUrlInParams {
423 Url: "",
424 }
425
426 err := decoder.Decode(&jsonIn)
427 if err != nil {
428 panic(err)
429 }
430 if jsonIn.Url == "" || (strings.Index(strings.Trim(jsonIn.Url, " "), "http://") != 0 && strings.Index(strings.Trim(jsonIn.Url, " "), "https://") != 0) {
431 w.WriteHeader(http.StatusBadRequest)
432 log.Println("400 - Bad request")
433 json.NewEncoder(w).Encode(map[string]string{
434 "code" : "400",
435 "status" : "Bad request",
436 })
437 } else {
438
439 params := mux.Vars(r)
440
441 db := connectToDb();
442
443 newLink := putUserLinkIntoDB(params["userId"], jsonIn.Url, db);
444
445 w.Header().Set("Content-Type", "application/json")
446
447 var putUrl AnswerTargetLink
448 putUrl.TargetLink.Id = newLink.Id
449
450 json.NewEncoder(w).Encode(putUrl)
451 }
452}
453
454/**
455 Redirect to real url by shortcode
456 */
457func (a *ApiFacade) redirectByShortCode(w http.ResponseWriter, r *http.Request) {
458 fmt.Println("redirectByShortCode")
459
460 params := mux.Vars(r)
461
462 db := connectToDb();
463
464 dbLink := findLinkByShortcode(params["shortcode"], db);
465
466 if dbLink.OriginalUrl != "" {
467
468 // Track event "clicked shortlink"
469 shortenedLink := struct {
470 Id string
471 } {
472 Id: dbLink.ShortenedUrl,
473 }
474
475 trackEvent(TrackedEvent{
476 Name: "shortlink_clicked",
477 Shortened: shortenedLink,
478 })
479
480 http.Redirect(w, r, dbLink.OriginalUrl, 301)
481 }
482
483 w.Header().Set("Content-Type", "text/html")
484
485 body := []byte("<h4>Hmm...?</h4>")
486
487 w.Write(body)
488}
489
490/**
491 Tracked event
492 */
493type TrackedEvent struct {
494 Name string
495 Shortened interface{} // @TODO Do correct ))
496}
497
498/**
499 Track event
500 */
501func trackEvent(event TrackedEvent) {
502 // @TODO Track an event...
503}
504
505func main() {
506 r := mux.NewRouter()
507
508 log.Print("Service started...")
509
510 var a ApiFacade
511
512 r.HandleFunc("/{userId}/register", a.registerUserUrl).Methods("PUT")
513 r.HandleFunc("/{userId}/{linkId}/shorten", a.shortUserLink).Methods("POST")
514 r.HandleFunc("/{shortcode}", a.redirectByShortCode).Methods("GET")
515
516 log.Print("Routes map created...")
517
518 log.Fatal(http.ListenAndServe(":8001", r))
519}