DEV Community

Cover image for Attempting to Learn Go - Building a Downloader Part 05
Steve Layton
Steve Layton

Posted on • Originally published at shindakun.glitch.me on

Attempting to Learn Go - Building a Downloader Part 05

This post is going to be a bit different than the previous ones. We're only going to make a small update to our Go code. This is mainly to take care of some of the sanitization worries I pointed out last time. Once we're done with that we'll layout the code we need to create an extension for Chrome! And on that note lets get going!

Pass Six

We're going to start by adding in the Sanitize package by kennygrant. To add a package in Go we need to use go get from the command line.

go get github.com/kennygrant/sanitize
Enter fullscreen mode Exit fullscreen mode

We can then add it to our import list as shown in the code below. Sanitize adds functions which will help in sanitizing text strings. Exactly what we are looking for! This should help remove any unwanted characters from the files and directories we attempt to create.

Small update, as @rafaacioly points out, I can use the built-in http.StatusBadRequest and http.StatusInternalServerError on my HTTP errors. It's more characters but I believe it's "correct" to use the built-in values.

package main

import (  
  "encoding/json"
  "fmt"
  "io"
  "io/ioutil"
  "log"
  "net/http"
  "net/url"
  "os"
  "path/filepath"
  "strings"

  "github.com/kennygrant/sanitize"
)

type download struct {  
  Title string `json:"title"`
  Location string `json:"location"`
}

func status(response http.ResponseWriter, request *http.Request) {  
  fmt.Fprintf(response, "Hello!")
}

func handleDownloadRequest(response http.ResponseWriter, request *http.Request) {  
  var downloadRequest download
  r, err := ioutil.ReadAll(request.Body)
  if err != nil {
    http.Error(response, "bad request", http.StatusBadRequest)
    log.Println(err)
    return
  }
  defer request.Body.Close()

  err = json.Unmarshal(r, &downloadRequest)
  if err != nil {
    http.Error(response, "bad request: "+err.Error(), http.StatusBadRequest)
    return
  }
  log.Printf("%#v", downloadRequest)

  err = getFile(downloadRequest)
  if err != nil {
    http.Error(response, "internal server error: "+err.Error(), http.StatusInternalServerError)
    return
  }

  fmt.Fprintf(response, "Download!")
}

func createSaveDirectory(title string) (string, error) {  
  dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
  if err != nil {
    log.Fatal(err)
  }
  path := filepath.Join(dir, title)
  _, err = os.Stat(path)

  // directory does not exist we should create it.
  if err != nil {
    err = os.Mkdir(path, 0755)
    if err != nil {
      log.Fatal(err)
    }
  }

  return path, nil
}
Enter fullscreen mode Exit fullscreen mode

As you can see below, we are calling sanitize in two places. The first, sanitize.BaseName, is when passing the title to the createSaveDirectory function. The second, sanitize.Path, when attempting to actually save the file with os.Create. The part should take care of removing any characters we don't want. For instance, if someone tries a directory transversal with the title by submitting the JSON "title": "Fake Title ../../../../../etc/". The save directory will still be created as a subfolder where our application resides, it will just have the name Fake-Title-etc-. The filename is sanitized slightly differently but the end result is basically the same. I didn't do an exhaustive test but for now, we should be OK.

func getFile(downloadRequest download) error {  
  u, err := url.Parse(downloadRequest.Location)
  if err != nil {
    log.Println(err)
    return err
  }

  save, err := createSaveDirectory(sanitize.BaseName(downloadRequest.Title))
  if err != nil {
    log.Println(err)
    return err
  }

  // Encoding URL via path never seems to work as expected, fall back to
  // simply replacing spaces with %20's, for now.
  response, err := http.Get(strings.Replace(downloadRequest.Location, " ", "%20", -1))
  if err != nil {
    log.Println(err)
    return err
  }
  defer response.Body.Close()

  out, err := os.Create(filepath.Join(save, sanitize.Path(filepath.Base(u.Path))))
  defer out.Close()
  _, err = io.Copy(out, response.Body)
  if err != nil {
    log.Println(err)
    return err
  }
  return nil
}

func main() {  
  log.Println("Downloader")

  http.HandleFunc("/", status)
  http.HandleFunc("/download", handleDownloadRequest)
  http.ListenAndServe(":3000", nil)
}
Enter fullscreen mode Exit fullscreen mode

That wraps up the Go portion of this post. Now, we're going to do a crash course in writing Chrome extensions! I'm not going to go too deep and for now, will only cover the three core files that make up the extension. For a deeper look at creating an extension check out the extension section on the Chrome developers portal.


Chrome Extension

An extension is typically made up of three or four files. The manifest.json, is the most important, it provides the browser with all the information it needs to load the extension. The permissions and the background entry are probably the most important sections within the file to note for now. Also, note that we are using localhost for our testing.

{
  "manifest_version": 2,
  "name": "Send to Downloader",
  "short_name": "Send2DL",
  "description": "Sends JSON to download server.",
  "version": "0.0.1",
  "minimum_chrome_version": "38",
  "permissions": [
    "webRequest",
    "*://localhost/",
    "contextMenus",
    "activeTab"],
  "icons": {
    "16": "assets/download.png"
  },
  "background": {"page": "background.html"}
}
Enter fullscreen mode Exit fullscreen mode

I'm sure you are surprised to see our background.html below. That's right it doesn't do much of anything, except load main.js, which is actually a pretty big deal.

<!DOCTYPE html>  
<html>  
  <body>
    <script src="main.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

main.js is the heart of our extension, as it actually takes care of the of putting our request together. It also handles adding our download option to Chrome's right-click context menu. Looking close you can see that the "Send to downloader" option will only appear if you right click on an image. In its current form, the code simply takes the title of the current tab and uses that as the directory you want to save the image in. If I ever take this past a learning exercise I'd like to extend this to any link and file type.

sendToDownloader = function(obj, tab) {  
  let downloaderUrl = 'http://localhost:3000/download';

  let title = tab.title;
  let imgUrl = obj.srcUrl;

  let payload = {
    title: title,
    location: imgUrl
  };

  let xhr = new XMLHttpRequest();

  xhr.open("POST", downloaderUrl, false);
  xhr.setRequestHeader('Content-type','application/json; charset=utf-8');
  xhr.send(JSON.stringify(payload));

  let result = xhr.responseText;
};

chrome.contextMenus.create({  
  title: "Send to downloader",
  contexts:["image"],
  onclick: sendToDownloader
});
Enter fullscreen mode Exit fullscreen mode

That brings us almost to the end of this exercise. The final post will cover the process of actually deploying the application on Google Compute engine. We'll finally be able to see the whole thing in action! After that, I may poke around with the downloader further but, I probably won't document too much outside of what is committed to GitHub.

Until next time...


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.




Top comments (2)

Collapse
 
rafaacioly profile image
Rafael Acioly

super nice! :)

just a hint; you can use http.StatusBadRequest instead 400 and http.StatusInternalServerError for 500, all status has a enum(?) for each one.

Collapse
 
shindakun profile image
Steve Layton

Thanks for the comment! I Totally forgot that they are all provided already in the HTTP package. Will update. :D