aboutsummaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/.Dockerignore1
-rw-r--r--backend/Dockerfile26
-rw-r--r--backend/api/api.go57
-rw-r--r--backend/api/ip.go37
-rw-r--r--backend/api/routes.go58
-rw-r--r--backend/cache/cache.go44
-rw-r--r--backend/db/db.go68
-rw-r--r--backend/db/mongo.go67
-rw-r--r--backend/db/schemas.go15
-rw-r--r--backend/go.mod13
-rw-r--r--backend/hashing/hash.go26
-rw-r--r--backend/main.go11
12 files changed, 423 insertions, 0 deletions
diff --git a/backend/.Dockerignore b/backend/.Dockerignore
new file mode 100644
index 0000000..10d4fb7
--- /dev/null
+++ b/backend/.Dockerignore
@@ -0,0 +1 @@
+frontend \ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..b533070
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,26 @@
+## Build stage
+FROM golang:alpine AS builder
+ENV GO111MODULE=on
+
+# Copy files to image
+COPY . /app/src
+WORKDIR /app/src
+
+RUN apk add git ca-certificates
+
+# Build image
+RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app
+
+## Image creation stage
+FROM scratch
+
+# Copy app
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+COPY --from=builder /go/bin/app ./
+COPY .env ./
+
+# Expose ports, change port to whatever you need to expose
+EXPOSE 8080
+
+# Run app
+ENTRYPOINT ["./app"] \ No newline at end of file
diff --git a/backend/api/api.go b/backend/api/api.go
new file mode 100644
index 0000000..c197774
--- /dev/null
+++ b/backend/api/api.go
@@ -0,0 +1,57 @@
+package api
+
+import (
+ "net/http"
+ "os"
+ "os/signal"
+ "strconv"
+ "syscall"
+ "time"
+
+ mux "github.com/gorilla/mux"
+ log "github.com/sirupsen/logrus"
+)
+
+func cleanup() {
+ log.Print("Shutting down server...")
+}
+
+// Define router and start server
+func Serve(port int) {
+ // Sigint trapper
+ c := make(chan os.Signal)
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+ go func() {
+ <-c
+ cleanup()
+ os.Exit(0)
+ }()
+
+ // Define Mux Router
+ r := mux.NewRouter()
+ r.HandleFunc("/health", healthCheckFunc)
+ r.HandleFunc("/api", insertFunc).Methods("POST")
+ r.HandleFunc("/api/{hash}", getHashFunc).Methods("GET")
+
+ http.Handle("/", r)
+
+ // Start HTTP server
+ server := newServer(":"+strconv.Itoa(port), r)
+ log.Printf("Starting server on %d", port)
+
+ defer cleanup()
+ err := server.ListenAndServe()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+// Function to create new HTTP server
+func newServer(addr string, router http.Handler) *http.Server {
+ return &http.Server{
+ Addr: addr,
+ Handler: router,
+ ReadTimeout: time.Second * 30,
+ WriteTimeout: time.Second * 30,
+ }
+}
diff --git a/backend/api/ip.go b/backend/api/ip.go
new file mode 100644
index 0000000..0d135b3
--- /dev/null
+++ b/backend/api/ip.go
@@ -0,0 +1,37 @@
+package api
+
+import (
+ "net"
+ "net/http"
+ "strings"
+)
+
+func getIP(r *http.Request) (s string) {
+ // Get IP from the X-REAL-IP header
+ ip := r.Header.Get("X-REAL-IP")
+ netIP := net.ParseIP(ip)
+ if netIP != nil {
+ return ip
+ }
+
+ // Get IP from X-FORWARDED-FOR header
+ ips := r.Header.Get("X-FORWARDED-FOR")
+ splitIps := strings.Split(ips, ",")
+ for _, ip := range splitIps {
+ netIP := net.ParseIP(ip)
+ if netIP != nil {
+ return ip
+ }
+ }
+
+ // Get IP from RemoteAddr
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return
+ }
+ netIP = net.ParseIP(ip)
+ if netIP != nil {
+ return
+ }
+ return
+}
diff --git a/backend/api/routes.go b/backend/api/routes.go
new file mode 100644
index 0000000..760ee35
--- /dev/null
+++ b/backend/api/routes.go
@@ -0,0 +1,58 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/jackyzha0/ctrl-v/cache"
+ "github.com/jackyzha0/ctrl-v/db"
+
+ log "github.com/sirupsen/logrus"
+)
+
+func healthCheckFunc(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "status ok")
+}
+
+func insertFunc(w http.ResponseWriter, r *http.Request) {
+ // get content
+ _ = r.ParseMultipartForm(0)
+ expiry := r.FormValue("expiry")
+ content := r.FormValue("content")
+
+ // get ip
+ ip := getIP(r)
+
+ log.Infof("got content '%s' and ip '%s'", content, ip)
+
+ // insert content
+ err := db.New(ip, content, expiry)
+ if err != nil {
+ fmt.Fprintf(w, "got err: %s", err.Error())
+ }
+}
+
+func getHashFunc(w http.ResponseWriter, r *http.Request) {
+ hash := mux.Vars(r)["hash"]
+ paste, err := cache.C.Get(hash)
+
+ // if hash was not found
+ if err != nil {
+ w.WriteHeader(http.StatusNotFound)
+ fmt.Fprintf(w, "got err: %s", err.Error())
+ return
+ }
+
+ // otherwise, return paste content and current time
+ w.Header().Set("Content-Type", "application/json")
+ pasteMap := map[string]interface{}{
+ "timestamp": time.Now(),
+ "content": paste.Content,
+ }
+
+ jsonData, _ := json.Marshal(pasteMap)
+ fmt.Fprintf(w, "%+v", string(jsonData))
+}
diff --git a/backend/cache/cache.go b/backend/cache/cache.go
new file mode 100644
index 0000000..bac7ea8
--- /dev/null
+++ b/backend/cache/cache.go
@@ -0,0 +1,44 @@
+package cache
+
+import (
+ "sync"
+
+ "github.com/jackyzha0/ctrl-v/db"
+)
+
+type Cache struct {
+ m map[string]db.Paste
+ lock sync.RWMutex
+}
+
+var C *Cache
+
+func init() {
+ C = &Cache{
+ m: map[string]db.Paste{},
+ }
+}
+
+func (c *Cache) Get(hash string) (db.Paste, error) {
+ c.lock.RLock()
+
+ // check if hash in cache
+ v, ok := c.m[hash]
+ c.lock.RUnlock()
+
+ if ok {
+ return v, nil
+ }
+
+ // if it doesnt, lookup from db
+ p, err := db.Lookup(hash)
+ c.add(p)
+ return p, err
+}
+
+func (c *Cache) add(p db.Paste) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ c.m[p.Hash] = p
+}
diff --git a/backend/db/db.go b/backend/db/db.go
new file mode 100644
index 0000000..053ba87
--- /dev/null
+++ b/backend/db/db.go
@@ -0,0 +1,68 @@
+package db
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/jackyzha0/ctrl-v/hashing"
+ "github.com/joho/godotenv"
+ log "github.com/sirupsen/logrus"
+)
+
+func init() {
+ // load .env file
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error loading .env file: %s", err.Error())
+ }
+
+ mUser := os.Getenv("MONGO_USER")
+ mPass := os.Getenv("MONGO_PASS")
+ mIP := os.Getenv("MONGO_SHARD_URL")
+
+ initSessions(mUser, mPass, mIP)
+}
+
+// creates a new paste with content and hash
+func New(ip, content, expiry string) error {
+ // generate hash from ip
+ hash := hashing.GenerateURI(ip)
+
+ // create new struct
+ new := Paste{
+ Hash: hash,
+ Content: content,
+ }
+
+ // check if expiry
+ if expiry != "" {
+ t, err := time.Parse(time.RFC3339, expiry)
+
+ // if time format not current
+ if err != nil {
+ return err
+ }
+
+ // time is in the past
+ if t.After(time.Now()) {
+ return fmt.Errorf("err: time %s is in the past", t.String())
+ }
+
+ new.Expiry = t
+
+ } else {
+ // 5 year expiry
+ new.Expiry = time.Now().Add(time.Hour * 43800)
+ }
+
+ // insert struct
+ log.Infof("create new paste with hash %s", hash)
+ insertErr := insert(new)
+ return insertErr
+}
+
+// lookup
+func Lookup(hash string) (Paste, error) {
+ return fetch(hash)
+}
diff --git a/backend/db/mongo.go b/backend/db/mongo.go
new file mode 100644
index 0000000..4c8a739
--- /dev/null
+++ b/backend/db/mongo.go
@@ -0,0 +1,67 @@
+package db
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+
+ "github.com/globalsign/mgo"
+ "github.com/globalsign/mgo/bson"
+ log "github.com/sirupsen/logrus"
+)
+
+var Session *mgo.Session
+var pastes *mgo.Collection
+
+func initSessions(user, pass, ip string) {
+ log.Infof("attempting connection to %s", ip)
+
+ // build uri string
+ URIfmt := "mongodb://%s:%s@%s:27017"
+ mongoURI := fmt.Sprintf(URIfmt, user, pass, ip)
+ dialInfo, err := mgo.ParseURL(mongoURI)
+ if err != nil {
+ log.Fatalf("error parsing uri: %s", err.Error())
+ }
+
+ tlsConfig := &tls.Config{}
+ dialInfo.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) {
+ conn, err := tls.Dial("tcp", addr.String(), tlsConfig)
+ return conn, err
+ }
+
+ Session, err = mgo.DialWithInfo(dialInfo)
+ if err != nil {
+ log.Fatalf("error establishing connection to mongo: %s", err.Error())
+ }
+
+ // ensure expiry check
+ sessionTTL := mgo.Index{
+ Key: []string{"expiry"},
+ ExpireAfter: 0,
+ }
+
+ // ensure hashes are unique
+ uniqueHashes := mgo.Index{
+ Key: []string{"hash"},
+ Unique: true,
+ }
+
+ _ = Session.DB("main").C("pastes").EnsureIndex(sessionTTL)
+ _ = Session.DB("main").C("pastes").EnsureIndex(uniqueHashes)
+
+ // Define connection to Databases
+ pastes = Session.DB("main").C("pastes")
+}
+
+func insert(new Paste) error {
+ return pastes.Insert(new)
+}
+
+func fetch(hash string) (Paste, error) {
+ p := Paste{}
+
+ q := bson.M{"hash": hash}
+ err := pastes.Find(q).One(&p)
+ return p, err
+}
diff --git a/backend/db/schemas.go b/backend/db/schemas.go
new file mode 100644
index 0000000..bdfa60c
--- /dev/null
+++ b/backend/db/schemas.go
@@ -0,0 +1,15 @@
+package db
+
+import (
+ "time"
+
+ "github.com/globalsign/mgo/bson"
+)
+
+// Paste represents a single paste
+type Paste struct {
+ ID bson.ObjectId `bson:"_id,omitempty"`
+ Hash string
+ Content string
+ Expiry time.Time `bson:"expiry"`
+}
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 0000000..fc4b4dd
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,13 @@
+module github.com/jackyzha0/ctrl-v
+
+go 1.13
+
+require (
+ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
+ github.com/gorilla/mux v1.7.4
+ github.com/joho/godotenv v1.3.0
+ github.com/kr/pretty v0.2.0 // indirect
+ github.com/sirupsen/logrus v1.6.0
+ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+)
diff --git a/backend/hashing/hash.go b/backend/hashing/hash.go
new file mode 100644
index 0000000..400659e
--- /dev/null
+++ b/backend/hashing/hash.go
@@ -0,0 +1,26 @@
+package hashing
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "math/big"
+ "time"
+)
+
+const UrlLength = 7
+
+// GenerateURI creates a unique identifier for a paste based on ip and timestamp
+func GenerateURI(ip string) string {
+ timeStamp := time.Now().String()
+ return hashString(ip + timeStamp)[:UrlLength]
+}
+
+// hashes using MD5 and then converts to base 62
+func hashString(text string) string {
+ hash := md5.Sum([]byte(text))
+ hexStr := hex.EncodeToString(hash[:])
+
+ bi := big.NewInt(0)
+ bi.SetString(hexStr, 16)
+ return bi.Text(62)
+} \ No newline at end of file
diff --git a/backend/main.go b/backend/main.go
new file mode 100644
index 0000000..18e141d
--- /dev/null
+++ b/backend/main.go
@@ -0,0 +1,11 @@
+package main
+
+import (
+ "github.com/jackyzha0/ctrl-v/api"
+ _ "github.com/jackyzha0/ctrl-v/cache" // setup cache
+ _ "github.com/jackyzha0/ctrl-v/db" // setup db
+)
+
+func main() {
+ api.Serve(8080)
+}