diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..11d5bbe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*
+!/**/
+!*.*
+!Dockerfile
diff --git a/Phase-05/Dockerfile b/Phase-05/Dockerfile
new file mode 100644
index 0000000..83f762d
--- /dev/null
+++ b/Phase-05/Dockerfile
@@ -0,0 +1,29 @@
+#
+FROM golang:1.22.5-alpine3.20 AS build
+
+WORKDIR /app
+
+RUN adduser -D -g '' -u 10001 builder
+RUN chown -R builder:builder /app
+USER builder
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY *.go ./
+RUN go build -o ./traceroute-api
+#
+
+#
+FROM alpine:3.20.0 AS final
+
+WORKDIR /app
+
+LABEL org.opencontainers.image.source=https://github.com/Star-Academy/Summer1403-Devops-Team12
+
+COPY --from=build --chown=root:root /app/traceroute-api ./traceroute-api
+
+EXPOSE 8080
+
+CMD ["./traceroute-api"]
+#
diff --git a/Phase-05/README.md b/Phase-05/README.md
new file mode 100644
index 0000000..47d4f63
--- /dev/null
+++ b/Phase-05/README.md
@@ -0,0 +1,31 @@
+# How to run
+
+## build
+```bash
+$ go build -o main
+```
+
+## Run
+```bash
+# Listening to ICMP packets requires root privileges
+$ sudo ./main
+```
+
+## Usage
+```bash
+$ curl http://localhost:8080/trace/{ip}
+```
+```bash
+$ curl http://localhost:8080/trace/{ip}?maxHops={max_hops}
+```
+
+## Example
+```bash
+$ curl http://localhost:8080/trace/8.8.8.8
+```
+```bash
+$ curl http://localhost:8080/trace/google.com
+```
+```bash
+$ curl http://localhost:8080/trace/8.8.8.8?maxHops=10
+```
\ No newline at end of file
diff --git a/Phase-05/config.go b/Phase-05/config.go
new file mode 100644
index 0000000..6893cdf
--- /dev/null
+++ b/Phase-05/config.go
@@ -0,0 +1,7 @@
+package main
+
+import (
+ "os"
+)
+
+var redisConnStr = defaultString(os.Getenv("REDIS_CONN_STR"), "redis://localhost:6379")
diff --git a/Phase-05/dto.go b/Phase-05/dto.go
new file mode 100644
index 0000000..2670e89
--- /dev/null
+++ b/Phase-05/dto.go
@@ -0,0 +1,19 @@
+package main
+
+type TraceHopResponse struct {
+ Hop int `json:"hop"`
+ IPAddr string `json:"ip"`
+ RTT int64 `json:"rtt"`
+}
+
+func (hop *TraceHop) toTraceHopResponse(hopIndex int) *TraceHopResponse {
+ if hop == nil {
+ return &TraceHopResponse{Hop: hopIndex + 1, IPAddr: "", RTT: -1}
+ } else {
+ return &TraceHopResponse{
+ Hop: hopIndex + 1,
+ IPAddr: hop.IPAddr.String(),
+ RTT: hop.RTT.Milliseconds(),
+ }
+ }
+}
diff --git a/Phase-05/go.mod b/Phase-05/go.mod
new file mode 100644
index 0000000..1f232b2
--- /dev/null
+++ b/Phase-05/go.mod
@@ -0,0 +1,11 @@
+module phase05
+
+go 1.22.5
+
+require (
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/go-redis/redis/v8 v8.11.5 // indirect
+ golang.org/x/net v0.27.0 // indirect
+ golang.org/x/sys v0.22.0 // indirect
+)
diff --git a/Phase-05/go.sum b/Phase-05/go.sum
new file mode 100644
index 0000000..320b9ba
--- /dev/null
+++ b/Phase-05/go.sum
@@ -0,0 +1,10 @@
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/Phase-05/main.go b/Phase-05/main.go
new file mode 100644
index 0000000..c8a3cdc
--- /dev/null
+++ b/Phase-05/main.go
@@ -0,0 +1,13 @@
+package main
+
+import "log"
+
+func main() {
+ err := initRedis()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println("Successfully connected to Redis")
+
+ RunTraceRouteServer(":8080")
+}
diff --git a/Phase-05/server.go b/Phase-05/server.go
new file mode 100644
index 0000000..b0a1409
--- /dev/null
+++ b/Phase-05/server.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+var DomainRegex = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
+var AddrRegex = regexp.MustCompile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`)
+
+func traceRouteHandler(w http.ResponseWriter, r *http.Request) {
+ path, _, _ := strings.Cut(r.URL.Path, "?")
+ trimmedPath := strings.Trim(path, "/")
+ addr := trimmedPath[strings.LastIndex(trimmedPath, "/")+1:]
+
+ if !AddrRegex.MatchString(addr) && !DomainRegex.MatchString(addr) {
+ WriteBadRequest(w, "Invalid addr "+addr)
+ return
+ }
+
+ maxHops, err := strconv.Atoi(defaultString(r.URL.Query().Get("maxHops"), "30"))
+ if err != nil {
+ WriteBadRequest(w, "maxHops must be an integer")
+ return
+ }
+
+ hops, err := TraceRoute(addr, maxHops)
+ if err != nil {
+ WriteError(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ result := make([]*TraceHopResponse, len(hops))
+ for i, hop := range hops {
+ result[i] = hop.toTraceHopResponse(i)
+ }
+
+ response, err := WriteJSON(w, result)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ err = saveToRedis(GenerateRedisKey(addr, maxHops), response)
+ if err != nil {
+ log.Println(err)
+ }
+}
+
+func RunTraceRouteServer(listen string) {
+ handler := &RegexpHandler{}
+ handler.HandleFunc(regexp.MustCompile(`^/trace/[^/]+$`), traceRouteHandler)
+
+ log.Printf("Listening on %s\n", listen)
+ err := http.ListenAndServe(listen, handler)
+ if err != nil {
+ log.Fatalf("Failed to start server. %v", err)
+ }
+}
diff --git a/Phase-05/storage.go b/Phase-05/storage.go
new file mode 100644
index 0000000..697ef39
--- /dev/null
+++ b/Phase-05/storage.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/go-redis/redis/v8"
+)
+
+var (
+ ctx = context.Background()
+ rdb *redis.Client = nil
+)
+
+func initRedis() error {
+ if rdb == nil {
+ opts, err := redis.ParseURL(redisConnStr)
+ if err != nil {
+ return err
+ }
+
+ rdb = redis.NewClient(opts)
+ return rdb.Ping(ctx).Err()
+ }
+
+ return nil
+}
+
+func saveToRedis(key string, value []byte) error {
+ if rdb == nil {
+ log.Println("Redis not initialized. Initializing now...")
+ initRedis()
+ }
+
+ err := rdb.Set(ctx, key, value, 0).Err()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/Phase-05/traceroute.go b/Phase-05/traceroute.go
new file mode 100644
index 0000000..7703302
--- /dev/null
+++ b/Phase-05/traceroute.go
@@ -0,0 +1,133 @@
+// Copyright © 2016 Alex
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option) any
+// later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package main
+
+import (
+ "crypto/rand"
+ "fmt"
+ "net"
+ "os"
+ "time"
+
+ "golang.org/x/net/icmp"
+ "golang.org/x/net/ipv4"
+)
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+const (
+ ProtocolICMP = 1
+ //ProtocolIPv6ICMP = 58
+ ListenAddr = "0.0.0.0"
+)
+
+type TraceHop struct {
+ IPAddr net.IP
+ RTT time.Duration
+}
+
+func TraceRoute(addr string, maxHops int) ([]*TraceHop, error) {
+ c, err := icmp.ListenPacket("ip4:icmp", ListenAddr)
+ if err != nil {
+ panic(err)
+ }
+ defer c.Close()
+
+ // Resolve any DNS (if used) and get the real IP of the target
+ dst, err := net.ResolveIPAddr("ip4", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ res := make([]*TraceHop, maxHops)
+
+ for i := 0; i < maxHops; i++ {
+ finished, dst, dur, err := getNthHop(c, dst, i+1)
+ if err != nil {
+ res[i] = nil
+ } else {
+ res[i] = &TraceHop{IPAddr: dst.IP, RTT: dur}
+ if finished {
+ return res[:i+1], nil
+ }
+ }
+
+ }
+
+ return res, nil
+}
+
+// Mostly based on https://github.com/golang/net/blob/master/icmp/ping_test.go
+// All ye beware, there be dragons below...
+
+func getNthHop(c *icmp.PacketConn, dst *net.IPAddr, ttl int) (bool, *net.IPAddr, time.Duration, error) {
+ // Start listening for icmp replies
+ c.IPv4PacketConn().SetTTL(ttl)
+
+ data := make([]byte, 64)
+ rand.Read(data)
+
+ // Make a new ICMP message
+ m := icmp.Message{
+ Type: ipv4.ICMPTypeEcho, Code: 0,
+ Body: &icmp.Echo{
+ ID: os.Getpid() & 0xffff, Seq: 1, //<< uint(seq), // TODO
+ Data: data,
+ },
+ }
+ b, err := m.Marshal(nil)
+ if err != nil {
+ return false, dst, 0, err
+ }
+
+ // Send it
+ start := time.Now()
+
+ n, err := c.WriteTo(b, dst)
+ if err != nil {
+ return false, dst, 0, err
+ } else if n != len(b) {
+ return false, dst, 0, fmt.Errorf("got %v; want %v", n, len(b))
+ }
+
+ // Wait for a reply
+ reply := make([]byte, 1500)
+ err = c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
+ if err != nil {
+ return false, dst, 0, err
+ }
+ n, peer, err := c.ReadFrom(reply)
+ if err != nil {
+ // fmt.Println("Unable to read!")
+ return false, dst, 0, err
+ }
+ duration := time.Since(start)
+
+ // Pack it up boys, we're done here
+ rm, err := icmp.ParseMessage(ProtocolICMP, reply[:n])
+ if err != nil {
+ return false, dst, 0, err
+ }
+ switch rm.Type {
+ case ipv4.ICMPTypeEchoReply:
+ return true, dst, duration, nil
+ case ipv4.ICMPTypeTimeExceeded:
+ // Convert peer to IPAddr
+ return false, &net.IPAddr{IP: peer.(*net.IPAddr).IP}, duration, nil
+ default:
+ return false, dst, 0, fmt.Errorf("got %+v from %v; want echo reply", rm, peer)
+ }
+}
diff --git a/Phase-05/utils.go b/Phase-05/utils.go
new file mode 100644
index 0000000..24b3cad
--- /dev/null
+++ b/Phase-05/utils.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "regexp"
+ "time"
+)
+
+func defaultString(s, def string) string {
+ if s == "" {
+ return def
+ }
+ return s
+}
+
+type route struct {
+ pattern *regexp.Regexp
+ handler http.Handler
+}
+
+type RegexpHandler struct {
+ routes []*route
+}
+
+func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.Handler) {
+ h.routes = append(h.routes, &route{pattern, handler})
+}
+
+func (h *RegexpHandler) HandleFunc(pattern *regexp.Regexp, handler func(http.ResponseWriter, *http.Request)) {
+ h.routes = append(h.routes, &route{pattern, http.HandlerFunc(handler)})
+}
+
+func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ for _, route := range h.routes {
+ if route.pattern.MatchString(r.URL.Path) {
+ route.handler.ServeHTTP(w, r)
+ return
+ }
+ }
+ // no pattern matched; send 404 response
+ http.NotFound(w, r)
+}
+
+func WriteBadRequest(w http.ResponseWriter, msg string) []byte {
+ resp := WriteError(w, msg, http.StatusBadRequest)
+ log.Printf("Bad request: %s\n", resp)
+ return resp
+}
+
+func WriteError(w http.ResponseWriter, msg string, code int) []byte {
+ w.WriteHeader(code)
+ resp, _ := json.Marshal(map[string]string{"error": msg})
+
+ log.Printf("%s: %s\n", http.StatusText(code), resp)
+
+ w.Write(resp)
+ return resp
+}
+
+func WriteJSON(w http.ResponseWriter, data interface{}) ([]byte, error) {
+ w.Header().Set("Content-Type", "application/json")
+ jsonData, err := json.Marshal(data)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return nil, err
+ }
+
+ w.Write(jsonData)
+
+ return jsonData, nil
+}
+
+func GenerateRedisKey(addr string, maxHops int) string {
+ timestamp := time.Now().Unix()
+ return fmt.Sprintf("%s:%d:%d", addr, maxHops, timestamp)
+}