Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phase05 #8

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*
!/**/
!*.*
!Dockerfile
29 changes: 29 additions & 0 deletions Phase-05/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# <Build stage>
FROM golang:1.22.5-alpine3.20 AS build

WORKDIR /app

RUN adduser -D -g '' -u 10001 builder

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

من جایی دیدم که نباید به صورت دستی یک UID مشخص ست کنیم. حالا نمیدونم چطوری هم بالای 10000 باشه و هم دستی ست نشه.

Copy link
Collaborator Author

@Alirezaja1384 Alirezaja1384 Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

من مثلا ایمیج gitea rootless رو میبینم uid تنظیم شده و روی 1000 هم تنظیم شده. عجیبه.
@dkhorasanizadeh
شما نظری ندارید؟

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
# </Build stage>

# <Final stage>
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"]
# </Final stage>
31 changes: 31 additions & 0 deletions Phase-05/README.md
Original file line number Diff line number Diff line change
@@ -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
```
7 changes: 7 additions & 0 deletions Phase-05/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import (
"os"
)

var redisConnStr = defaultString(os.Getenv("REDIS_CONN_STR"), "redis://localhost:6379")
19 changes: 19 additions & 0 deletions Phase-05/dto.go
Original file line number Diff line number Diff line change
@@ -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(),
}
}
}
11 changes: 11 additions & 0 deletions Phase-05/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 10 additions & 0 deletions Phase-05/go.sum
Original file line number Diff line number Diff line change
@@ -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=
13 changes: 13 additions & 0 deletions Phase-05/main.go
Original file line number Diff line number Diff line change
@@ -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")
}
62 changes: 62 additions & 0 deletions Phase-05/server.go

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

به هیچ وجه مشکلی حساب نمیشه و صرفا برام سوال بود که چرا از gin استفاده نکردید؟ :)

Copy link
Collaborator Author

@Alirezaja1384 Alirezaja1384 Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

چون API ای که ازمون میخواستن ساده بود. با هم که صحبت کردیم در نهایت به این نتجه رسیدیم gin لازم نیست و net/http کافیه. جدای از این با این کار بیشتر با golang آشنا شدیم.

Original file line number Diff line number Diff line change
@@ -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])$`)
Comment on lines +11 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

بهتر نبود این دو تا متغیر داخل توابع مشخصی تعریف و همونجا برای چک کردن رجکس استفاده بشن که مجبور نباشید متغیر ها رو گلوبال تعریف کنید؟

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

به ۲ دلیل انجام دادم. یکی اینکه هربار regex کامپایل نشه و و فقط موقع startup این کار انجام بشه و یکی دیگه هم اینکه چون از MustCompile استفاده شده اگه regex مشکل داشته باشه موقع startup برنامه crash میکنه و آدم سریع متوجه اشکالش میشه.


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)
}
}
41 changes: 41 additions & 0 deletions Phase-05/storage.go
Original file line number Diff line number Diff line change
@@ -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
}
133 changes: 133 additions & 0 deletions Phase-05/traceroute.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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)
}
}
Loading