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
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
}
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)
}
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"}
}
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>
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
});
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.
shindakun / atlg
Source repo for the "Attempting to Learn Go" posts I've been putting up over on dev.to
Attempting to Learn Go
Here you can find the code I've been writing for my Attempting to Learn Go posts that I've been writing and posting over on Dev.to.
Post Index
Enjoy this post? |
---|
How about buying me a coffee? |
Top comments (2)
super nice! :)
just a hint; you can use
http.StatusBadRequest
instead400
andhttp.StatusInternalServerError
for500
, all status has aenum(?)
for each one.Thanks for the comment! I Totally forgot that they are all provided already in the HTTP package. Will update. :D