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