diff options
Diffstat (limited to 'mpaste.go')
-rw-r--r-- | mpaste.go | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/mpaste.go b/mpaste.go new file mode 100644 index 0000000..eec6a20 --- /dev/null +++ b/mpaste.go @@ -0,0 +1,262 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "strconv" + "strings" + "sync" + + "github.com/Mango0x45/getgopt" + "github.com/alecthomas/chroma/formatters/html" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" + "github.com/dgrijalva/jwt-go" +) + +const ( + URL_HOMEPAGE = iota + URL_INVALID + URL_SYNTAX + URL_VALID +) + +var ( + counter int + counter_file string + domain string + file_prefix string + index_file string + mutex sync.Mutex + secret_key = os.Getenv("MPASTE_SECRET") + user_file string +) + +var ( + style = styles.Get("pygments") + formatter = html.New(html.Standalone(true), html.WithClasses(true), + html.WithLineNumbers(true), html.LineNumbersInTable(true)) +) + +func usage() { + fmt.Fprintf(os.Stderr, + "Usage: %s [-c file] [-f directory] [-i file] [-u file] domain port\n", + os.Args[0]) + os.Exit(1) +} + +func error_and_die(e interface{}) { + fmt.Fprintln(os.Stderr, e) + os.Exit(1) +} + +func remove_ext(s string) string { + return strings.TrimSuffix(s, path.Ext(s)) +} + +func allowed_user(name string) bool { + mutex.Lock() + defer mutex.Unlock() + + if _, err := os.Stat(user_file); os.IsNotExist(err) { + return false + } + + file, err := os.Open(user_file) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return false + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + if scanner.Text() == name { + return true + } + } + + return false +} + +func validate_token(r *http.Request) bool { + token, _ := jwt.Parse(r.Header.Get("Authorization"), func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Something went wrong\n") + } + return []byte(secret_key), nil + }) + + if token == nil { + return false + } + + claims, ok := token.Claims.(jwt.MapClaims) + + if !(ok && token.Valid) { + return false + } + + if user_file == "" { + return true + } + + return allowed_user(claims["name"].(string)) +} + +func is_valid_url(s string) int { + var i int + var c rune + for i, c = range s { + if c == '.' && i > 0 { + return URL_SYNTAX + } else if c < '0' || c > '9' { + return URL_INVALID + } + } + + if c != 0 { + return URL_VALID + } + return URL_HOMEPAGE +} + +func syntax_highlighting(w http.ResponseWriter, r *http.Request) { + lexer := lexers.Match(r.URL.Path[1:]) + if lexer == nil { + http.ServeFile(w, r, file_prefix+r.URL.Path[1:]) + return + } + + data, err := ioutil.ReadFile(file_prefix + remove_ext(r.URL.Path[1:])) + if err != nil { + WRITE_HEADER(http.StatusNotFound, "404 page not found") + } + + iterator, err := lexer.Tokenise(nil, string(data)) + if err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to tokenize output") + } + + if err := formatter.Format(w, style, iterator); err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to format output") + } +} + +func endpoint(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + switch is_valid_url(r.URL.Path[1:]) { + case URL_HOMEPAGE: + http.ServeFile(w, r, index_file) + case URL_INVALID: + WRITE_HEADER(http.StatusNotFound, "404 page not found") + case URL_SYNTAX: + w.Header().Set("Content-Type", "text/html") + syntax_highlighting(w, r) + case URL_VALID: + w.Header().Set("Content-Type", "text/plain") + http.ServeFile(w, r, file_prefix+r.URL.Path[1:]) + } + case http.MethodPost: + if secret_key != "" && !validate_token(r) { + WRITE_HEADER(http.StatusForbidden, "Invalid API key") + } + + file, _, err := r.FormFile("data") + defer file.Close() + if err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to parse form") + } + + mutex.Lock() + + fname := file_prefix + strconv.Itoa(counter) + nfile, err := os.Create(fname) + defer nfile.Close() + if err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to create file") + } + + if _, err = io.Copy(nfile, file); err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to write file") + } + + if err = os.WriteFile(counter_file, []byte(strconv.Itoa(counter+1)), 0644); err != nil { + WRITE_HEADER(http.StatusInternalServerError, "Failed to update counter") + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, domain+"/%d\n", counter) + + counter++ + mutex.Unlock() + default: + WRITE_HEADER(http.StatusMethodNotAllowed, "Only GET and POST requests are supported") + } +} + +func main() { + for opt := byte(0); getgopt.Getopt(len(os.Args), os.Args, ":c:f:i:u:", &opt); { + switch opt { + case 'c': + counter_file = getgopt.Optarg + case 'f': + file_prefix = getgopt.Optarg + case 'i': + index_file = getgopt.Optarg + case 'u': + user_file = getgopt.Optarg + default: + usage() + } + } + + argv := os.Args[getgopt.Optind:] + if len(argv) != 2 { + usage() + } + domain = argv[0] + port := argv[1] + + if file_prefix == "" { + file_prefix = "files/" + } else if file_prefix[len(file_prefix)-1] != '/' { + file_prefix += "/" + } + + if index_file == "" { + index_file = "index.html" + } + + if _, err := os.Stat(index_file); os.IsNotExist(err) { + error_and_die(err) + } + + if _, err := os.Stat(file_prefix); os.IsNotExist(err) { + if err = os.MkdirAll(file_prefix, 0755); err != nil { + error_and_die(err) + } + } + + if _, err := os.Stat(counter_file); os.IsNotExist(err) { + counter = 0 + } else { + data, err := ioutil.ReadFile(counter_file) + if err != nil { + error_and_die(err) + } + counter, _ = strconv.Atoi(string(data)) + } + + http.HandleFunc("/", endpoint) + error_and_die(http.ListenAndServe(":"+port, nil)) +} |