-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Phase05 #8
Changes from 10 commits
dbd32d0
3abfca8
3c4d335
2ddcad5
db5f5fa
3be9952
ee86ed3
8737e0a
f027e6d
4157fbd
55af17a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
* | ||
!/**/ | ||
!*.* | ||
!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 | ||
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> |
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 | ||
``` |
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") |
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(), | ||
} | ||
} | ||
} |
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 | ||
) |
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= |
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") | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. به هیچ وجه مشکلی حساب نمیشه و صرفا برام سوال بود که چرا از gin استفاده نکردید؟ :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. چون API ای که ازمون میخواستن ساده بود. با هم که صحبت کردیم در نهایت به این نتجه رسیدیم gin لازم نیست و |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. بهتر نبود این دو تا متغیر داخل توابع مشخصی تعریف و همونجا برای چک کردن رجکس استفاده بشن که مجبور نباشید متغیر ها رو گلوبال تعریف کنید؟ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.Fatal(err) | ||
Alirezaja1384 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
|
||
"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 { | ||
initRedis() | ||
} | ||
|
||
err := rdb.Set(ctx, key, value, 0).Err() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
من جایی دیدم که نباید به صورت دستی یک UID مشخص ست کنیم. حالا نمیدونم چطوری هم بالای 10000 باشه و هم دستی ست نشه.
There was a problem hiding this comment.
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
شما نظری ندارید؟