Technology is getting more enjoyable and interesting way day by day. Options are getting more with the day for solving problems. This can be understood in both bad and good. Most of the time there is always a better way of solving a problem.
less talk more action!
This post will try to give brief information about how to develop a simple Web app using Golang & WebAssembly. We will develop an app that will encrypt the URL that we gave and create a new encrypted URL for redirection. For encryption & decryption, we will use the Blowfish Encryption Algorithm (Don't need to know about the details of this algorithm).
Project: ghost-url.netlify.app
Source code: github.com/ParvinEyvazov/ghost-url (don't forget to leave a star⭐)
The project mainly consists of 3 parts.
- Logic (Go)
- UI (HTML/CSS and a bit js)
- WebServer (Go)
1. Project/Folder Setup
├ cmd
│ |-- wasm
│ | |-- main.go
│ |-- webserver
│ | |-- main.go
├ core
│ |-- core.go
│ |-- decrypt.go
│ |-- encrypt.go
├ static
│ |-- index.html
│ |-- loadwasm.js
│ |-- main.wasm
│ |-- styles.css
│ |-- wasm_exec.js
├ url
| |-- url.go
go.mod
go.sum
2. How does it work overall?
We are developing the logic code using Go and compile to the .wasm (WebAssembly) file. Inside our js scripts, we load this .wasm file and run the functions that we wrote. And let's get into the codes.
3.1. Logic (Go)
Our logic codes are in cmd/wasm/main.go
file. Here is the codes:
cmd/wasm/main.go
package main
import (
"log"
"syscall/js"
"github.com/ParvinEyvazov/ghost-url/core"
"github.com/ParvinEyvazov/ghost-url/url"
)
func main() {
done := make(chan struct{}, 0)
js.Global().Set("ghostUrl", js.FuncOf(ghostUrl))
js.Global().Set("recoverUrl", js.FuncOf(recoverUrl))
<-done
}
func ghostUrl(this js.Value, args []js.Value) interface{} {
urlText := (args[0]).String()
isValid := url.Valid(urlText)
if !isValid {
return ""
}
encryptedText, err := core.Encrypt(urlText)
if err != nil {
return ""
}
finalURL := url.Create(encryptedText)
return finalURL
}
func recoverUrl(this js.Value, args []js.Value) interface{} {
encrptedFullUrlText := (args[0]).String()
encrptedUrlText := url.Parse(encrptedFullUrlText)
decryptedURL, err := core.Decrypt(encrptedUrlText)
if err != nil {
log.Panic("ERROR: Decryption error", err)
}
return decryptedURL
}
Basically, we are creating 2 functions to use in our js.
- ghostUrl
- recoverUrl
ghostUrl is encrypting the URL provided by the user and returns the encrypted one. When a user comes with the encrypted URL to the app, recoverUrl is decrypting the URL and returning to js and inside js, the page redirects the user to this URL. This is kind of working like URL shorteners, but not saving them to the Database.
js.Global().Set("ghostUrl", js.FuncOf(ghostUrl))
js.Global().Set("recoverUrl", js.FuncOf(recoverUrl))
These codes are working kind of bridge functionality from Go (compiled .wasm) to the Js file.
These are the Blowfish encryption and decryption codes:
core/core.go
package core
import (
"crypto/cipher"
"encoding/base64"
"golang.org/x/crypto/blowfish"
)
const _KEY string = "suPerHarD!To*FinD"
func encodeBase64(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
func blowfishChecksizeAndPad(value []byte) []byte {
modulus := len(value) % blowfish.BlockSize
if modulus != 0 {
padnglen := blowfish.BlockSize - modulus
for i := 0; i < padnglen; i++ {
value = append(value, 0)
}
}
return value
}
func blowfishEncrypt(value, key []byte) ([]byte, error) {
bcipher, err := blowfish.NewCipher(key)
if err != nil {
return nil, err
}
returnMe := make([]byte, blowfish.BlockSize+len(value))
eiv := returnMe[:blowfish.BlockSize]
ecbc := cipher.NewCBCEncrypter(bcipher, eiv)
ecbc.CryptBlocks(returnMe[blowfish.BlockSize:], value)
return returnMe, nil
}
func encryptToByte(value string) ([]byte, error) {
var returnMe, valueInByteArr, paddedByteArr, keyByteArr []byte
valueInByteArr = []byte(value)
keyByteArr = []byte(_KEY)
paddedByteArr = blowfishChecksizeAndPad(valueInByteArr)
returnMe, err := blowfishEncrypt(paddedByteArr, keyByteArr)
if err != nil {
return nil, err
}
return returnMe, nil
}
func decodeBase64(s string) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
return data, nil
}
func blowfishDecrypt(value, key []byte) ([]byte, error) {
dcipher, err := blowfish.NewCipher(key)
if err != nil {
return nil, err
}
div := value[:blowfish.BlockSize]
decrypted := value[blowfish.BlockSize:]
if len(decrypted)%blowfish.BlockSize != 0 {
return nil, err
}
dcbc := cipher.NewCBCDecrypter(dcipher, div)
dcbc.CryptBlocks(decrypted, decrypted)
return decrypted, nil
}
func decryptToByte(value string) ([]byte, error) {
var returnMe, keyByteArr []byte
keyByteArr = []byte(_KEY)
decodeB64, err1 := decodeBase64(value)
if err1 != nil {
return nil, err1
}
returnMe, err2 := blowfishDecrypt(decodeB64, keyByteArr)
if err2 != nil {
return nil, err2
}
return returnMe, nil
}
For Decryption (from string to string)
core/decrypt.go
package core
func Decrypt(value string) (string, error) {
decryptedByteArr, err := decryptToByte(value)
if decryptedByteArr == nil {
return "", err
}
return string(decryptedByteArr[:]), nil
}
For Encryption (from string to string)
core/encrypt.go
package core
func Decrypt(value string) (string, error) {
decryptedByteArr, err := decryptToByte(value)
if decryptedByteArr == nil {
return "", err
}
return string(decryptedByteArr[:]), nil
}
For the URL operations
url/url.go
package url
import (
"net/url"
"strings"
)
type Routes map[string]string
var RoutesMap Routes = Routes{
"encryption": "/",
"decryption": "?d=",
}
var _SPLITTER string = RoutesMap["decryption"]
// var HOST string = "http://localhost:3000"
var HOST string = "https://ghost-url.netlify.app"
func Valid(text string) (isValid bool) {
_, err := url.ParseRequestURI(text)
if err == nil {
isValid = true
}
return
}
func Create(encrypted string) string {
return HOST + _SPLITTER + encrypted
}
func Parse(text string) (encrypted string) {
texts := strings.Split(text, _SPLITTER)
encrypted = texts[1]
return
}
3.2. UI (HTML/CSS and a bit js)
For UI we will surely use the building blocks of Web application which is HTML and CSS. For communicating between WebAssembly and UI we will use a bit of JavaScript. Let's start with the HTML file. static/index.html
is the file starting point.
static/index.html
<html>
<head>
<link rel="stylesheet" href="styles.css" />
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<script src="loadWasm.js"></script>
</head>
<body>
<div class="name-container">
<h1>Ghost Url 👻</h1>
</div>
<div class="container">
<div class="input-container">
<div class="error-message-container">
<h1 id="error-message"></h1>
</div>
<div class="webflow-style-input">
<input
type="text"
id="url"
name="url"
placeholder="type url here..."
/>
</div>
<div class="button-container">
<a class="animated-button" id="create-button">
<span></span>
<span></span>
<span></span>
<span></span>
👻 ghost
</a>
</div>
<div
class="text-container webflow-style-input copy-text"
style="display: none"
>
<span id="short-url"
></span
>
<a class="animated-button copy-button">
<span></span>
<span></span>
<span></span>
<span></span>
copy
</a>
</div>
</div>
</div>
<script>
(async () => {
await init();
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params.d != undefined && params.d != "") {
let direct_url = recoverUrl(window.location.href);
if (direct_url != undefined && direct_url != "") {
window.location.replace(direct_url);
}
}
})().catch((e) => {
console.log(e);
});
var url = document.querySelector("#url");
var short_url = document.querySelector("#short-url");
var text_container = document.querySelector(".copy-text");
var error_message = document.querySelector("#error-message");
if (short_url.innerHTML == "" || short_url.innerHTML == undefined) {
text_container.style.display = "none";
}
document
.getElementById("create-button")
.addEventListener("click", async function () {
let a = ghostUrl(url.value);
if (a != "" && a != undefined) {
cleanError();
text_container.style.display = "inline-block";
short_url.innerHTML = a;
} else {
showError("Please enter a valid url");
}
});
text_container.addEventListener("click", () => {
copyMessage(short_url.innerHTML);
});
function showError(message) {
error_message.innerHTML = "Please enter a valid url";
}
function cleanError() {
error_message.innerHTML = "";
}
// copy to clipboard
function copyMessage(string) {
const selBox = document.createElement("textarea");
selBox.style.position = "fixed";
selBox.style.left = "0";
selBox.style.top = "0";
selBox.style.opacity = "0";
selBox.value = string;
document.body.appendChild(selBox);
selBox.focus();
selBox.select();
document.execCommand("copy");
document.body.removeChild(selBox);
}
</script>
</body>
</html>
Loading .wasm file for using:
static/loadwasm.js
async function init() {
const go = new Go();
const result = await WebAssembly.instantiateStreaming(
fetch("main.wasm"),
go.importObject
);
go.run(result.instance);
}
Style:
static/style.css
Would be better if you copy it from here (because of the number of lines): static/style.css
And a JavaScript file provided by Go to load your .wasm file into a Web page. It is a dynamic file, and because of that would be awesome if you always install the latest version of it from Go itself. You can also copy it from the URL provided below.
static/wasm_exec.js
There are 2 ways.
- Copy from Go source (run this in project folder terminal):
ORcp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./static/
- Create a
static/wasm_exec.js
file and copy from here
3.3. WebServer (Go)
It will be a basic HTTP file server written in Go. Here, we will basically serve our ./static file. Here is the code:
cmd/webserver/main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/ParvinEyvazov/ghost-url/url"
)
type IServer interface {
start()
}
type Server struct {
}
func main() {
server := NewServer()
server.start()
}
func (*Server) start() {
fmt.Println()
http.Handle(url.RoutesMap["encryption"], EncryptionHandler())
log.Println("Listening on http://localhost:3000")
err := http.ListenAndServe("localhost:3000", nil)
if err != nil {
log.Fatal(err)
}
}
func EncryptionHandler() http.Handler {
return http.FileServer(http.Dir("./static"))
}
func NewServer() IServer {
return &Server{}
}
4. Run the project
We finally completed the code part and let's run our project:
- Create .mod file
go mod init <project name>
go mod tidy
Don't forget to update names of the packages in the imports sections of
cmd\wasm\main.go
and cmd\webserver\main.go
.
For my scenario
go mod init github.com/ParvinEyvazov/ghost-url
go mod tidy
- Build the .wasm file (will compile it into ./static file)
GOOS=js GOARCH=wasm go build -o static/main.wasm cmd/wasm/main.go
- Run the project
go run ./cmd/webserver/main.go
- Open in browser
If any error occurs on running or building steps, please take a look at the source code here: github.com/ParvinEyvazov/ghost-url
My local Golang version is 1.18.1
That is all you need to run Go on Web UI using WebAssembly.
I hope this blog will be helpful to you. Thanks for reading and have a wonderful day :)