package main //go:generate go get github.com/go-bindata/go-bindata //go:generate go get -u github.com/go-bindata/go-bindata/... //go:generate go-bindata -pkg $GOPACKAGE assets/ import ( "bytes" "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "html/template" "io" "log" "net/http" "net/url" "regexp" "runtime/debug" "sort" "strconv" "strings" "time" "unicode" "gitea.hashru.nl/dsprenkels/rushlink/gobmarsh" "github.com/gorilla/mux" "github.com/pkg/errors" bolt "go.etcd.io/bbolt" ) type PasteType int type PasteState int type StoredPaste struct { Type PasteType State PasteState Content []byte Key []byte OwnerToken [16]byte TimeCreated time.Time } type HTMLPage struct { Title string Content string } const ( TypePaste PasteType = iota TypeRedirect ) const ( StatePresent PasteState = iota StateDeleted ) const CookieOwnerToken = "owner_token" // These keys are designated reserved, and will not be randomly chosen var ReservedPasteKeys [][]byte = [][]byte{[]byte("xd42"), []byte("example")} // Base64 encoding and decoding var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" var base64Encoder = base64.NewEncoding(base64Alphabet).WithPadding(base64.NoPadding) // Asset contents var layoutContents = string(MustAsset("assets/layout.html")) var indexContents = string(MustAsset("assets/index.txt")) // Templates var HtmlTemplate = template.Must(template.New("layout").Parse(layoutContents)) var acceptHeaderMediaRangeRegex = regexp.MustCompile(`^\s*([^()<>@,;:\\"/\[\]?.=]+)/([^()<>@,;:\\"/\[\]?.=]+)\s*$`) var acceptHeaderAcceptParamsRegex = regexp.MustCompile(`^\s*(\w+)=([A-Za-z0-9.-])\s*$`) var acceptHeaderWeight = regexp.MustCompile(`^\s*q=0(?:\.([0-9]{0,3}))|1(?:\.0{0,3})\s*$`) func (t PasteType) String() (string, error) { switch t { case TypePaste: return "paste", nil case TypeRedirect: return "redirect", nil default: return "", fmt.Errorf("invalid PasteType (%v)", t) } } func (t PasteState) String() (string, error) { switch t { case StatePresent: return "present", nil case StateDeleted: return "deleted", nil default: return "", fmt.Errorf("invalid PasteState (%v)", t) } } func indexGetHandler(w http.ResponseWriter, r *http.Request) { page := HTMLPage{Title: "rushlink", Content: indexContents} RenderPage(w, r, page) } func indexPostHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(50 * 1000 * 1000); err != nil { log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } // Determine what kind of post this is, currently only `shorten=...` if len(r.PostForm) == 0 { RenderError(w, r, http.StatusBadRequest, "empty body in POST request\n") return } shorten_values, prs := r.PostForm["shorten"] if !prs { RenderError(w, r, http.StatusBadRequest, "no 'shorten' param supplied\n") return } if len(shorten_values) != 1 { RenderError(w, r, http.StatusBadRequest, "only one 'shorten' param is allowed per request\n") return } shortenPostHandler(w, r) } func pasteGetHandler(w http.ResponseWriter, r *http.Request) { pasteGetHandlerInner(w, r, false, false) } func pasteGetHandlerNoRedirect(w http.ResponseWriter, r *http.Request) { pasteGetHandlerInner(w, r, true, false) } func pasteGetHandlerMeta(w http.ResponseWriter, r *http.Request) { pasteGetHandlerInner(w, r, false, true) } func pasteGetHandlerInner(w http.ResponseWriter, r *http.Request, noRedirect, showMeta bool) { vars := mux.Vars(r) key := vars["key"] var storedPaste *StoredPaste if err := db.View(func(tx *bolt.Tx) error { var err error storedPaste, err = getURL(tx, []byte(key)) return err }); err != nil { log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } if storedPaste == nil { RenderError(w, r, http.StatusNotFound, "url key not found in the database\n") return } if showMeta { typeString, err := storedPaste.Type.String() if err != nil { log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } stateString, err := storedPaste.State.String() if err != nil { log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } isOwner := "no" ownerToken, ok := getOwnerTokenFromRequest(r) if ok && subtle.ConstantTimeCompare(ownerToken[:], storedPaste.OwnerToken[:]) == 1 { isOwner = "yes" } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "key: %v\n", string(storedPaste.Key)) fmt.Fprintf(w, "type: %v\n", typeString) fmt.Fprintf(w, "state: %v\n", stateString) fmt.Fprintf(w, "created: %v\n", storedPaste.TimeCreated.String()) fmt.Fprintf(w, "are you the owner: %v\n", isOwner) return } switch storedPaste.State { case StatePresent: if !noRedirect { rawurl := string(storedPaste.Content) urlParse, err := url.Parse(rawurl) if err != nil { log.Printf("error: invalid URL ('%v') in database for key '%v': %v\n", rawurl, storedPaste.Key, err) msg := fmt.Sprintf("internal server error: invalid url in database\n") RenderError(w, r, http.StatusInternalServerError, msg) return } http.Redirect(w, r, urlParse.String(), http.StatusSeeOther) } w.Write(storedPaste.Content) case StateDeleted: RenderError(w, r, http.StatusGone, "paste has been deleted\n") return default: w.WriteHeader(http.StatusInternalServerError) log.Printf("error: invalid storedPaste.State (%v) for key '%v'\n", storedPaste.State, storedPaste.Key) msg := fmt.Sprintf("internal server error: invalid storedPaste.State (%v)\n", storedPaste.State) RenderError(w, r, http.StatusInternalServerError, msg) return } } func shortenPostHandler(w http.ResponseWriter, r *http.Request) { rawurl := r.PostForm.Get("shorten") userURL, err := url.ParseRequestURI(rawurl) if err != nil { msg := fmt.Sprintf("invalid url (%v): %v\n", err, rawurl) RenderError(w, r, http.StatusBadRequest, msg) return } if userURL.Scheme == "" { msg := fmt.Sprintf("invalid url (unspecified scheme)\n", rawurl) RenderError(w, r, http.StatusBadRequest, msg) return } if userURL.Host == "" { msg := fmt.Sprintf("invalid url (unspecified host)\n", rawurl) RenderError(w, r, http.StatusBadRequest, msg) return } var storedPaste *StoredPaste if err := db.Update(func(tx *bolt.Tx) error { ownerKey, ok := getOwnerTokenFromRequest(r) if ok == false { // Owner key not supplied or invalid, generate a new one ownerKey, err = generateOwnerToken() if err != nil { return errors.Wrap(err, "generating OwnerToken") } } sp, err := shortenURL(tx, userURL, ownerKey) storedPaste = sp return err }); err != nil { log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } saveURL, err := r.URL.Parse(string(storedPaste.Key)) if err != nil { err = errors.Wrap(err, "parsing url") log.Printf("error: %v\n", err) msg := fmt.Sprintf("internal server error: %v\n", err) RenderError(w, r, http.StatusInternalServerError, msg) return } var base64OwnerToken = make([]byte, 24) base64Encoder.Encode(base64OwnerToken, storedPaste.OwnerToken[:]) isNotPrint := func(r rune) bool { return !unicode.IsPrint(r) } ownerKey := strings.TrimRightFunc(string(base64OwnerToken), isNotPrint) page := HTMLPage{ Title: fmt.Sprintf("URL saved at %v\n", saveURL), Content: fmt.Sprintf("URL saved at %v\nOwner key is %s\n", saveURL, ownerKey), } RenderPage(w, r, page) } // Retrieve a URL from the database func getURL(tx *bolt.Tx, key []byte) (*StoredPaste, error) { shortenBucket := tx.Bucket([]byte(BUCKET_PASTES)) if shortenBucket == nil { return nil, fmt.Errorf("bucket %v does not exist", BUCKET_PASTES) } storedBytes := shortenBucket.Get(key) if storedBytes == nil { return nil, nil } storedPaste := &StoredPaste{} err := gobmarsh.Unmarshal(storedBytes, storedPaste) return storedPaste, err } // Add a new URL to the database // // Returns the new ID if the url was successfully shortened func shortenURL(tx *bolt.Tx, userURL *url.URL, ownerKey [16]byte) (*StoredPaste, error) { shortenBucket := tx.Bucket([]byte(BUCKET_PASTES)) if shortenBucket == nil { return nil, fmt.Errorf("bucket %v does not exist", BUCKET_PASTES) } // Generate a key until it is not in the database, this occurs in O(log N), // where N is the amount of keys stored in the url-shorten database. epoch := 0 var urlKey []byte for { var err error urlKey, err = generateURLKey(epoch) if err != nil { return nil, errors.Wrap(err, "url-key generation failed") } found := shortenBucket.Get(urlKey) if found == nil { break } isReserved := false for _, reservedKey := range ReservedPasteKeys { if bytes.HasPrefix(urlKey, reservedKey) { isReserved = true break } } if !isReserved { break } epoch++ } // Store the new key storedPaste := StoredPaste{ Type: TypeRedirect, State: StatePresent, Content: []byte(userURL.String()), Key: urlKey, OwnerToken: ownerKey, TimeCreated: time.Now().UTC(), } storedBytes, err := gobmarsh.Marshal(storedPaste) if err != nil { return nil, errors.Wrap(err, "encoding for database failed") } if err := shortenBucket.Put(urlKey, storedBytes); err != nil { return nil, errors.Wrap(err, "database transaction failed") } return &storedPaste, nil } func generateURLKey(epoch int) ([]byte, error) { urlKey := make([]byte, 4+epoch) _, err := rand.Read(urlKey) if err != nil { return nil, err } // Put all the values in the range 0..64 for easier base64-encoding for i := 0; i < len(urlKey); i++ { urlKey[i] &= 0x3F } // Implement truncate-resistance by forcing the prefix to // 0b111110xxxxxxxxxx // ^----- {epoch} ones followed by a single 0 // // Example when epoch is 1: prefix is 0b10. i := 0 for i < epoch { // Set this bit to 1 limb := i / 6 bit := i % 6 urlKey[limb] |= 1 << uint(5-bit) i++ } // Finally set the next bit to 0 limb := i / 6 bit := i % 6 urlKey[limb] &= ^(1 << uint(5-bit)) // Convert this ID to a canonical base64 notation for i := range urlKey { urlKey[i] = base64Alphabet[urlKey[i]] } return urlKey, nil } func generateOwnerToken() ([16]byte, error) { var ownerKey [16]byte _, err := rand.Read(ownerKey[:]) if err != nil { return ownerKey, err } return ownerKey, nil } func getOwnerTokenFromRequest(r *http.Request) ([16]byte, bool) { var ownerKey [16]byte ownerKeyCookie, err := r.Cookie(CookieOwnerToken) if err != nil && err != http.ErrNoCookie { return ownerKey, false } if ownerKeyCookie != nil { n, err := base64Encoder.Strict().Decode(ownerKey[:], []byte(ownerKeyCookie.Value)) if err == nil || n == 16 { return ownerKey, true } } return ownerKey, false } func RenderPage(w http.ResponseWriter, r *http.Request, page HTMLPage) { contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"}) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("error: %v\n", err) fmt.Fprintf(w, "internal server error: %v\n", err) } switch contentType { case "text/plain": w.Header().Set("Content-Type", "text/plain") io.WriteString(w, page.Content) case "text/html": w.Header().Set("Content-Type", "text/html") err = HtmlTemplate.Execute(w, page) default: w.WriteHeader(http.StatusNotAcceptable) io.WriteString(w, "could not resolve an acceptable content-type\n") } if err != nil { panic(err) } } func RenderError(w http.ResponseWriter, r *http.Request, status int, msg string) { statusText := http.StatusText(status) if statusText == "" { statusText = "Unkown" } w.WriteHeader(status) page := HTMLPage{ Title: fmt.Sprintf("%v %v", status, statusText), Content: msg, } RenderPage(w, r, page) } // Try to resolve the preferred content-type for the response to this request. // // This is done by reading from the `types` argument. If one of them matches // the preferences supplied by the client in their Accept header, we will // return that one. We will take the clients preferences into account. // // Iff no match could be found, this function will return an empty string, and // the caller should probably respond with a 406 Not Acceptable status code. // Iff the Accept header was invalid, we will return an error. In this case, // the situation calls for a 400 Bad Request. func resolveResponseContentType(r *http.Request, types []string) (string, error) { // Ref: https://tools.ietf.org/html/rfc7231#section-5.3.2 if len(types) == 0 { return "", nil } acceptHeader := r.Header.Get("Accept") if acceptHeader == "" { return types[0], nil } type AcceptValue struct { Type string Subtype string Weight int } avStrings := strings.Split(acceptHeader, ",") avs := make([]AcceptValue, len(avStrings)) for i, avString := range avStrings { av := AcceptValue{Weight: 1000} choiceParts := strings.Split(avString, ";") mediaRange := acceptHeaderMediaRangeRegex.FindStringSubmatch(choiceParts[0]) if mediaRange == nil { return "", fmt.Errorf("bad media-range (\"%v\")", choiceParts[0]) } av.Type = mediaRange[1] av.Subtype = mediaRange[2] // Go through the rest to see if there is a q=... parameter for choiceParts = choiceParts[1:]; len(choiceParts) > 0; choiceParts = choiceParts[1:] { // Try to parse the weight param weight := acceptHeaderWeight.FindStringSubmatch(choiceParts[0]) if weight != nil { if weight[1] == "" { av.Weight = 0 } else { var err error av.Weight, err = strconv.Atoi((weight[1] + "000")[:3]) if err != nil { log.Println("error: unreachable statement") debug.PrintStack() av.Weight = 1000 // Reset to default value } } break } // Check if this parameter is still invalid in any case acceptParams := acceptHeaderAcceptParamsRegex.FindStringSubmatchIndex(choiceParts[0]) if acceptParams == nil { return "", fmt.Errorf("bad accept-params (\"%v\")", choiceParts[0]) } } avs[i] = av } sort.SliceStable(avs, func(i, j int) bool { if avs[i].Weight > avs[j].Weight { return true } if avs[i].Type != "*" && avs[j].Type == "*" { return true } if avs[i].Subtype != "*" && avs[j].Subtype == "*" { return true } return false }) avArgs := make([]AcceptValue, len(types)) for i, fulltype := range types { split := strings.Split(fulltype, "/") if len(split) == 1 { avArgs[i] = AcceptValue{Type: split[0]} } else { avArgs[i] = AcceptValue{Type: split[0], Subtype: split[1]} } } for _, av := range avs { for j, avArg := range avArgs { if !(av.Type == avArg.Type || av.Type == "*" || avArg.Type == "*") { continue } if !(av.Subtype == avArg.Subtype || av.Subtype == "*" || avArg.Subtype == "*") { continue } return types[j], nil } } return "", nil }