Compare commits
	
		
			4 Commits
		
	
	
		
			main
			...
			conditiona
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 58bb2432fb | |||
| b5fdfcb49c | |||
| 3fcc46c4f9 | |||
| 6d386bf591 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,2 @@ | |||||||
| redis | redis | ||||||
| sus |  | ||||||
| .env | .env | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM golang:1.16 | FROM golang:1.18 | ||||||
|  |  | ||||||
| WORKDIR /go/src/app | WORKDIR /go/src/app | ||||||
| COPY . . | COPY . . | ||||||
|   | |||||||
| @@ -6,10 +6,7 @@ services: | |||||||
|     ports: [ "8430:80" ] |     ports: [ "8430:80" ] | ||||||
|     environment: |     environment: | ||||||
|       - SECRET=${SECRET} |       - SECRET=${SECRET} | ||||||
|       - MAX_AGE_MS=${MAX_AGE_MS} |  | ||||||
|     restart: unless-stopped |  | ||||||
|   redis: |   redis: | ||||||
|     hostname: sus-redis |  | ||||||
|     image: redis:7 |     image: redis:7 | ||||||
|     volumes: [ "./redis:/data" ] |     volumes: [ "./redis:/data" ] | ||||||
|     restart: unless-stopped |     ports: [ "6379:6379" ] | ||||||
|   | |||||||
							
								
								
									
										139
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								main.go
									
									
									
									
									
								
							| @@ -25,6 +25,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/go-redis/redis/v8" | 	"github.com/go-redis/redis/v8" | ||||||
| @@ -33,19 +34,17 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var client = redis.NewClient(&redis.Options{ | var client = redis.NewClient(&redis.Options{ | ||||||
| 	Addr:     "sus-redis:6379", | 	Addr:     "redis:6379", | ||||||
| 	Password: "", | 	Password: "", | ||||||
| 	DB:       0, | 	DB:       0, | ||||||
| }) | }) | ||||||
|  |  | ||||||
| var SECRET string | var SECRET string | ||||||
| var INDEX_GET_REDIRECT = "http://alv.cx" | var INDEX_GET_REDIRECT = "http://alv.cx" | ||||||
| var MAX_AGE_MS int64 = 500 |  | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	r := mux.NewRouter() | 	r := mux.NewRouter() | ||||||
| 	r.HandleFunc("/{shortlink}", shortlinkHandler) | 	r.HandleFunc("/{shortlink}", shortlinkHandler) | ||||||
| 	r.HandleFunc("/{shortlink}/", shortlinkHandler) |  | ||||||
| 	r.HandleFunc("/", indexHandler) | 	r.HandleFunc("/", indexHandler) | ||||||
|  |  | ||||||
| 	listenAddress := "0.0.0.0:80" | 	listenAddress := "0.0.0.0:80" | ||||||
| @@ -62,14 +61,6 @@ func main() { | |||||||
| 		INDEX_GET_REDIRECT = p | 		INDEX_GET_REDIRECT = p | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if p, ok := os.LookupEnv("MAX_AGE_MS"); ok { |  | ||||||
| 		if v, err := strconv.ParseInt(p, 10, 64); err != nil { |  | ||||||
| 			fmt.Printf("Unable to parse environment variable MAX_AGE_MS: %v\n", p) |  | ||||||
| 		} else { |  | ||||||
| 			MAX_AGE_MS = v |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	log.Fatal(http.ListenAndServe(listenAddress, r)) | 	log.Fatal(http.ListenAndServe(listenAddress, r)) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -84,30 +75,27 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { | |||||||
|  |  | ||||||
| 	command := r.PostForm.Get("Command") | 	command := r.PostForm.Get("Command") | ||||||
| 	shortlink := r.PostForm.Get("Shortlink") | 	shortlink := r.PostForm.Get("Shortlink") | ||||||
| 	value := r.PostForm.Get("Value") | 	redirect := r.PostForm.Get("Redirect") | ||||||
| 	req_timestamp := r.PostForm.Get("Timestamp") | 	alt_redirect := r.PostForm.Get("AltRedirect") | ||||||
| 	req_timestamp_int, err := strconv.ParseInt(req_timestamp, 10, 64) | 	alt_condition := r.PostForm.Get("AltCondition") | ||||||
| 	if err != nil { | 	fmt.Printf("command: %v, shortlink: %v, redirect: %v, alt_redirect: %v, alt_condition: %v\n", | ||||||
| 		w.WriteHeader(http.StatusBadRequest) | 		command, | ||||||
| 		w.Write([]byte("Bad request")) | 		shortlink, | ||||||
| 		return | 		redirect, | ||||||
| 	} | 		alt_redirect, | ||||||
|  | 		alt_condition, | ||||||
|  | 	) | ||||||
|  |  | ||||||
| 	cur_timestamp := time.Now().UnixNano() | 	formstring := command + ":" + shortlink + ":" + redirect + ":" + alt_condition + ":" + alt_redirect | ||||||
| 	if req_timestamp_int+MAX_AGE_MS*1000*1000 < cur_timestamp { |  | ||||||
| 		w.WriteHeader(http.StatusBadRequest) |  | ||||||
| 		w.Write([]byte("Bad request")) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	fmt.Printf("req_timestamp: %v, command: %v, shortlink: %v, value: %v\n", req_timestamp, command, shortlink, value) | 	fmt.Println("formstring: " + formstring) | ||||||
|  |  | ||||||
| 	signature := r.Header.Get("Signature") | 	signature := r.Header.Get("Signature") | ||||||
| 	calculatedSignature := fmt.Sprintf( | 	calculatedSignature := fmt.Sprintf( | ||||||
| 		"SUS-SIGNATURE-%v", | 		"SUS-SIGNATURE-%v", | ||||||
| 		getSha256HMACSignature( | 		getSha256HMACSignature( | ||||||
| 			[]byte(SECRET), | 			[]byte(SECRET), | ||||||
| 			req_timestamp+":"+command+":"+shortlink+":"+value, | 			command+":"+shortlink+":"+redirect+":"+alt_condition+":"+alt_redirect, | ||||||
| 		), | 		), | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| @@ -125,7 +113,21 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		ctx := context.Background() | 		ctx := context.Background() | ||||||
| 		_, err := client.Get(ctx, shortlink).Result() | 		_, err := client.Get(ctx, shortlink).Result() | ||||||
| 		if err == redis.Nil { | 		if err == redis.Nil { | ||||||
| 			err = client.Set(ctx, shortlink, value, 0).Err() | 			err = client.Set(ctx, shortlink+":redirect", shortlink, 0).Err() | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Println(err) | ||||||
|  | 				w.WriteHeader(500) | ||||||
|  | 				w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			err = client.Set(ctx, shortlink+":altcondition", alt_condition, 0).Err() | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Println(err) | ||||||
|  | 				w.WriteHeader(500) | ||||||
|  | 				w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			err = client.Set(ctx, shortlink+":altredirect", alt_redirect, 0).Err() | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				fmt.Println(err) | 				fmt.Println(err) | ||||||
| @@ -153,13 +155,16 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if command == "delete" { | 	if command == "delete" { | ||||||
| 		if value != "confirm" { |  | ||||||
| 			w.WriteHeader(400) |  | ||||||
| 			w.Write([]byte("400 Bad Request")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		ctx := context.Background() | 		ctx := context.Background() | ||||||
| 		if err := client.Del(ctx, shortlink).Err(); err != nil { | 		if err := client.Del(ctx, shortlink+":redirect").Err(); err != nil { | ||||||
|  | 			w.WriteHeader(500) | ||||||
|  | 			w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 		} | ||||||
|  | 		if err := client.Del(ctx, shortlink+":altredirect").Err(); err != nil { | ||||||
|  | 			w.WriteHeader(500) | ||||||
|  | 			w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 		} | ||||||
|  | 		if err := client.Del(ctx, shortlink+":altcondition").Err(); err != nil { | ||||||
| 			w.WriteHeader(500) | 			w.WriteHeader(500) | ||||||
| 			w.Write([]byte("500 Internal Server Error")) | 			w.Write([]byte("500 Internal Server Error")) | ||||||
| 		} | 		} | ||||||
| @@ -177,13 +182,13 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { | |||||||
|  |  | ||||||
| 		resp := "" | 		resp := "" | ||||||
| 		for _, key := range keys { | 		for _, key := range keys { | ||||||
| 			value, err = client.Get(ctx, key).Result() | 			shortlink, err = client.Get(ctx, key).Result() | ||||||
| 			if err == redis.Nil { | 			if err == redis.Nil { | ||||||
| 				w.WriteHeader(500) | 				w.WriteHeader(500) | ||||||
| 				w.Write([]byte("500 Internal Server Error")) | 				w.Write([]byte("500 Internal Server Error")) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			resp += key + ":" + value + "\n" | 			resp += key + ":" + shortlink + "\n" | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		w.WriteHeader(200) | 		w.WriteHeader(200) | ||||||
| @@ -196,7 +201,9 @@ func shortlinkHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 	fmt.Println("shortlinkHandler called") | 	fmt.Println("shortlinkHandler called") | ||||||
| 	shortlink := string(mux.Vars(r)["shortlink"]) | 	shortlink := string(mux.Vars(r)["shortlink"]) | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	redirect, err := client.Get(ctx, shortlink).Result() | 	client.Incr(ctx, shortlink + ":hits") | ||||||
|  |  | ||||||
|  | 	redirect, err := client.Get(ctx, shortlink+":redirect").Result() | ||||||
| 	fmt.Printf("shortlink: %v, redirect: %v\n", shortlink, redirect) | 	fmt.Printf("shortlink: %v, redirect: %v\n", shortlink, redirect) | ||||||
| 	if err == redis.Nil { | 	if err == redis.Nil { | ||||||
| 		w.WriteHeader(404) | 		w.WriteHeader(404) | ||||||
| @@ -204,13 +211,67 @@ func shortlinkHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		return | ||||||
| 	} else if err != nil { | 	} else if err != nil { | ||||||
| 		fmt.Println(err) | 		fmt.Println(err) | ||||||
|  | 		fmt.Println(0) | ||||||
| 		w.WriteHeader(500) | 		w.WriteHeader(500) | ||||||
| 		w.Write([]byte("500 Internal Server Error")) | 		w.Write([]byte("500 Internal Server Error")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	http.Redirect(w, r, redirect, 302) | 	altcondition, err := client.Get(ctx, shortlink+":altcondition").Result() | ||||||
| 	return | 	if err == redis.Nil { | ||||||
|  | 		http.Redirect(w, r, redirect, 302) | ||||||
|  | 		return | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		fmt.Println(1) | ||||||
|  | 		w.WriteHeader(500) | ||||||
|  | 		w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	altredirect, err := client.Get(ctx, shortlink+":altredirect").Result() | ||||||
|  | 	if err == redis.Nil { | ||||||
|  | 		http.Redirect(w, r, redirect, 302) | ||||||
|  | 		return | ||||||
|  | 	} else if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		fmt.Println(2) | ||||||
|  | 		w.WriteHeader(500) | ||||||
|  | 		w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	altcondition_split := strings.Split(altcondition, ",") | ||||||
|  | 	ac_varname := altcondition_split[0] | ||||||
|  | 	ac_operator := altcondition_split[1] | ||||||
|  | 	ac_required_value, _ := strconv.Atoi(altcondition_split[2]) | ||||||
|  |  | ||||||
|  | 	var ac_varval int | ||||||
|  |  | ||||||
|  | 	if ac_varname != "timestamp" { | ||||||
|  | 		ac_varvalstr, err := client.Get(ctx, shortlink+":"+ac_varname).Result() | ||||||
|  | 		if err == redis.Nil { | ||||||
|  | 			ac_varval = 0 | ||||||
|  | 		} else if err != nil { | ||||||
|  | 			fmt.Println(err) | ||||||
|  | 			fmt.Println(3) | ||||||
|  | 			w.WriteHeader(500) | ||||||
|  | 			w.Write([]byte("500 Internal Server Error")) | ||||||
|  | 			return | ||||||
|  | 		} else { | ||||||
|  | 			ac_varval, _ = strconv.Atoi(ac_varvalstr) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		ac_varval = int(time.Now().Unix()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ac_operator == "eq" && ac_varval == ac_required_value) || | ||||||
|  | 		(ac_operator == "gt" && ac_varval > ac_required_value) || | ||||||
|  | 		(ac_operator == "lt" && ac_varval < ac_required_value) { | ||||||
|  | 		http.Redirect(w, r, altredirect, 307) | ||||||
|  | 	} else { | ||||||
|  | 		http.Redirect(w, r, redirect, 307) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func getSha256HMACSignature(secret []byte, data string) string { | func getSha256HMACSignature(secret []byte, data string) string { | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								readme.md
									
									
									
									
									
								
							| @@ -7,11 +7,21 @@ sus URL shortener | |||||||
|  |  | ||||||
| - creating a new shortlink at https://pls.cx/shortlink | - creating a new shortlink at https://pls.cx/shortlink | ||||||
|  |  | ||||||
|         susmng [-s pls.cx] create -l shortlink -v https://example.com |         susmng [-s pls.cx] create -l shortlink -r https://example.com | ||||||
|  |  | ||||||
|  | - creating a new shortlink at https://pls.cx/shortlink which redirects to https://example.com/a the first n times, https://example.com/b any other times | ||||||
|  |  | ||||||
|  |         susmng [-s pls.cx] create -l shortlink -r https://example.com/a \ | ||||||
|  |                 -c hits,gt,<n> -a https://example.com/b | ||||||
|  |  | ||||||
|  | - creating a new shortlink at https://pls.cx/shortlink which redirects to https://example.com/before before unix timestamp n (seconds), https://example.com/after after that | ||||||
|  |  | ||||||
|  |         susmng [-s pls.cx] create -l shortlink -r https://example.com/before \ | ||||||
|  |                 -c timestamp,gt,<n> -a https://example.com/after | ||||||
|  |  | ||||||
| - deleting the shortlink https://pls.cx/shortlink | - deleting the shortlink https://pls.cx/shortlink | ||||||
|  |  | ||||||
|         susmng [-s pls.cx] delete -l shortlink -v confirm |         susmng [-s pls.cx] delete -l shortlink | ||||||
|  |  | ||||||
| - listing all shortlinks on the server pls.cx | - listing all shortlinks on the server pls.cx | ||||||
|  |  | ||||||
| @@ -33,14 +43,12 @@ flag is not provided. | |||||||
|  |  | ||||||
|         docker-compose up -d --build |         docker-compose up -d --build | ||||||
|  |  | ||||||
| ### server environment variables | #### server environment variables | ||||||
|  |  | ||||||
| | Variable             | Default         | Description                                                                            | | - `SECRET`---the secret used for signature verification | ||||||
| |----------------------|-----------------|----------------------------------------------------------------------------------------| | - `LISTEN_ADDRESS`---the address the server is listening on (default is `0.0.0.0:80`) | ||||||
| | `SECRET`             | N/A             | the secret used for signature verification                                             | | - `INDEX_GET_REDIRECT`---the URL the user should be redirected to if they try to access `/` on the | ||||||
| | `LISTEN_ADDRESS`     | `0.0.0.0:80`    | the address the server is listening on                                                 | |    server (default is `http://alv.cx`) | ||||||
| | `INDEX_GET_REDIRECT` | `http://alv.cx` | the URL the user should be redirected to if they try to access `/` on the server       | |  | ||||||
| | `MAX_AGE_MS`         | 500             | how old a request can be (in milliseconds) before the server will refuse to process it | |  | ||||||
|  |  | ||||||
| ### setting up susmng | ### setting up susmng | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								susmng.py
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								susmng.py
									
									
									
									
									
								
							| @@ -8,7 +8,6 @@ import pathlib | |||||||
| import os | import os | ||||||
| import json | import json | ||||||
| import sys | import sys | ||||||
| import time |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_args(): | def get_args(): | ||||||
| @@ -19,8 +18,10 @@ def get_args(): | |||||||
|     parser.add_argument('command') |     parser.add_argument('command') | ||||||
|     parser.add_argument('-s', '--server', default="") |     parser.add_argument('-s', '--server', default="") | ||||||
|     parser.add_argument('-l', '--shortlink', default="") |     parser.add_argument('-l', '--shortlink', default="") | ||||||
|     parser.add_argument('-v', '--value', default="") |     parser.add_argument('-r', '--redirect', default="") | ||||||
|     parser.add_argument('-c', '--config', type=pathlib.Path, default=pathlib.Path(os.path.expanduser('~/.config/susmng/config.json'))) |     parser.add_argument('-a', '--alt-redirect', default="") | ||||||
|  |     parser.add_argument('-c', '--alt-condition', default="") | ||||||
|  |     parser.add_argument('--config', type=pathlib.Path, default=pathlib.Path(os.path.expanduser('~/.config/susmng/config.json'))) | ||||||
|     parser.add_argument('-H', '--http', action='store_true') |     parser.add_argument('-H', '--http', action='store_true') | ||||||
|     return parser.parse_args() |     return parser.parse_args() | ||||||
|  |  | ||||||
| @@ -49,37 +50,25 @@ def main(args): | |||||||
|  |  | ||||||
|     secret = config['secrets'][server] |     secret = config['secrets'][server] | ||||||
|  |  | ||||||
|     if args.command == "delete" and args.value != "confirm": |     formstring = args.command+":"+args.shortlink+":"+args.redirect+":"+args.alt_condition+":"+args.alt_redirect | ||||||
|         print("--value not set to 'confirm'... delete operation may fail") |     print(f"{formstring=}") | ||||||
|  |  | ||||||
|     # accoring to python documentation (https://docs.python.org/3/library/time.html#time.time) |     r = requests.post(f"{'http' if args.http else 'https'}://{server}", | ||||||
|     # this function does not explicitly have to use unix time, and implementation is dependent |             data = { | ||||||
|     # platform. |                 'Command': args.command, | ||||||
|     # most platforms (windows, unix) will probably give unix time though. |                 'Shortlink': args.shortlink, | ||||||
|     # |                 'Redirect': args.redirect, | ||||||
|     # the server side (main.go file) does explicitly use unix time (time.Now().UnixNano()) to get |                 'AltCondition': args.alt_condition, | ||||||
|     # this number, but hopefully there should be no issues on most platforms. |                 'AltRedirect': args.alt_redirect, | ||||||
|     timestamp = str(time.time_ns()) |                 }, | ||||||
|  |             headers = { | ||||||
|     data = { |                 'Signature': 'SUS-SIGNATURE-' + hmac.new( | ||||||
|         'Command': args.command, |                     secret.encode("UTF-8"), | ||||||
|         'Shortlink': args.shortlink, |                     formstring.encode("UTF-8"), | ||||||
|         'Value': args.value, |                     hashlib.sha256 | ||||||
|         'Timestamp': timestamp, |                     ).hexdigest() | ||||||
|         } |                 } | ||||||
|  |             ) | ||||||
|     headers = { |  | ||||||
|         'Signature': 'SUS-SIGNATURE-' + hmac.new( |  | ||||||
|             secret.encode("UTF-8"), |  | ||||||
|             (timestamp + ":" + args.command + ":" + args.shortlink + ":" + args.value).encode("UTF-8"), |  | ||||||
|             hashlib.sha256 |  | ||||||
|             ).hexdigest() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     print(f"{data=}") |  | ||||||
|     print(f"{headers=}") |  | ||||||
|  |  | ||||||
|     r = requests.post(f"{'http' if args.http else 'https'}://{server}", data=data, headers=headers) |  | ||||||
|     print(r, file=sys.stderr) |     print(r, file=sys.stderr) | ||||||
|     print(r.content.decode().strip()) |     print(r.content.decode().strip()) | ||||||
|     return 0 |     return 0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user