Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Juno Website Hosting Update #939

Merged
merged 9 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, _ config.APIConfig) {
// register app's OpenAPI routes.
apiSvr.Router.Handle("/static/openapi.yml", http.FileServer(http.FS(docs.Docs)))
apiSvr.Router.HandleFunc("/", openapiconsole.Handler(Name, "/static/openapi.yml"))
apiSvr.Router.HandleFunc("/webhost", webhost.Handler())
apiSvr.Router.HandleFunc("/webhost/{contract}/{name}/{path:.*}", webhost.Handler())
}

// RegisterTxService implements the Application.RegisterTxService method.
Expand Down
20 changes: 15 additions & 5 deletions app/webhost/_scripts.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
juno-local
# EXECUTE THIS SCRIPT FROM THIS DIRECTORY

junod tx wasm store /home/reece/Desktop/cw-webhost/artifacts/cw_webhost.wasm --from=juno1 --keyring-backend=test --chain-id=local-1 --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes
junod tx wasm store cw_webhost.wasm --from=juno1 --keyring-backend=test --chain-id=local-1 --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes --home=/home/joel/.juno1

Reecepbcups marked this conversation as resolved.
Show resolved Hide resolved
junod tx wasm instantiate 1 '{}' --from=juno1 --keyring-backend=test --chain-id=local-1 --label=cw_webhost --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes --no-admin
sleep 3

junod tx wasm instantiate 1 '{}' --from=juno1 --keyring-backend=test --chain-id=local-1 --label=cw_webhost --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes --no-admin --home=/home/joel/.juno1
ADDRESS=juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8
Reecepbcups marked this conversation as resolved.
Show resolved Hide resolved

sleep 3

junod tx wasm execute $ADDRESS '{"new_website":{"name":"test","source":"<html><script>alert(\"popup\")</script><style>body {background-color: lightblue;}</style><h1>Test Website Header</h1><p>Paragraph</p></html>"}}' --from=juno1 --keyring-backend=test --chain-id=local-1 --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes
# Encode zip file to base64, generate new_web_cmd.txt
# ENSURE YOU HAVE A FILE NAMED 'web.zip' in the same directory as this script
python3 encode_zip_to_base64.py
JSON="$(cat new_web_cmd.txt)"
junod tx wasm execute $ADDRESS $JSON --from=juno1 --keyring-backend=test --chain-id=local-1 --gas=auto --gas-adjustment=1.5 --gas-prices=0.025ujuno --yes --home=/home/joel/.juno1

junod q wasm contract-state smart $ADDRESS '{"get_website":{"name":"test"}}'
sleep 3

junod q wasm contract-state smart $ADDRESS '{"get_website":{"name":"test"}}' --home=/home/joel/.juno1
Reecepbcups marked this conversation as resolved.
Show resolved Hide resolved

# Website should be available at: http://localhost:1317/webhost/juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8/test/
174 changes: 138 additions & 36 deletions app/webhost/console.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package webhost

import (
"archive/zip"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)

// TODO:
// https://github.com/rogchap/v8go ?
// Allow ZIP file upload into the contract? (easier to use images, else you need to use IPFS or direct links off chain)

const (
address = ":8787"
restServer = "http://0.0.0.0:1317/cosmwasm/wasm/v1/contract/"
address = ":8787"
restServer = "http://0.0.0.0:1317/cosmwasm/wasm/v1/contract/"
maxZipFileSize = uint64(1024 * 1024 * 5) // 5 MB
maxUnzippedFileSize = uint64(1024 * 1024 * 1) // 1 MB
)

type Website struct {
Expand All @@ -28,53 +27,156 @@ type Website struct {

func Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()

if len(params["contract"]) == 0 || len(params["name"]) == 0 {
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write([]byte("contract and name are required")); err != nil {
fmt.Println("error writing response: ", err)
}
return
}
// Get request route
route := strings.Split(r.URL.Path, "/")[2:]

contract := params["contract"][0]
name := params["name"][0]
// Pull out contract & name from route
contract := route[0]
name := route[1]

b64Query := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"get_website":{"name":"%s"}}`, name)))
// Determine file path, default to index.html if none specified
path := strings.Join(route[2:], "/")
if path == "" {
path = "index.html"
}

res, err := http.Get(restServer + contract + "/smart/" + b64Query)
// Send request to CosmWasm contract, retrieve website
website, err := makeRequest(contract, name)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error creating request: "+err.Error())
writeResponse(w, "error making request: "+err.Error())
return
}
defer res.Body.Close()

resBody, err := io.ReadAll(res.Body)
// Decode & unzip website source files
zr, err := decodeAndUnzip(website.Data.Source)
if err != nil {
fmt.Printf("client: could not read response body: %s\n", err)
os.Exit(1)
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error decoding and unzipping file: "+err.Error())
return
}

if len(resBody) > 0 && strings.Contains(string(resBody), "data") {
var website Website
err = json.Unmarshal(resBody, &website)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error unmarshalling response body: "+err.Error())
return
// Find file in zip at path
for _, f := range zr.File {
if f.Name == path {
rc, err := f.Open()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error opening file: "+err.Error())
return
}
defer rc.Close()

// Read file into buffer
buf := new(bytes.Buffer)

// Check if file size exceeds maximum allowed size
if f.UncompressedSize64 > maxUnzippedFileSize {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "resource exceeds maximum allowed size of "+fmt.Sprintf("%d", maxUnzippedFileSize)+" bytes")
return
}

// Define limit to prevent linting error (we check for file size limit above)
maxSize := int64(f.UncompressedSize64)

// Copy file to buffer
if _, err := io.CopyN(buf, rc, maxSize); err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error reading file: "+err.Error())
return
}

// Set content type based on file type
setContentType(w, path)

// Write buffer to response
if _, err := w.Write(buf.Bytes()); err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeResponse(w, "error writing response: "+err.Error())
}

break
}
}
}
}

writeResponse(w, website.Data.Source)
return
// Make request to contract through CosmWasm REST server
func makeRequest(contract string, name string) (Website, error) {
var website Website

b64Query := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"get_website":{"name":"%s"}}`, name)))

res, err := http.Get(restServer + contract + "/smart/" + b64Query)
if err != nil {
return website, err
}
defer res.Body.Close()

resBody, err := io.ReadAll(res.Body)
if err != nil {
return website, err
}

if len(resBody) > 0 && strings.Contains(string(resBody), "data") {
if err = json.Unmarshal(resBody, &website); err != nil {
return website, err
}
}

return website, nil
}

// Decode from base 64 and unzip file, return zip reader
func decodeAndUnzip(source string) (*zip.Reader, error) {
// Decode from base 64
decodedBytes, err := base64.StdEncoding.DecodeString(source)
if err != nil {
return nil, err
}

// Check if decoded content exceeds the maximum allowed size
if uint64(len(decodedBytes)) > maxZipFileSize {
return nil, fmt.Errorf("decoded content exceeds maximum allowed size of %d bytes", maxZipFileSize)
}

// Unzip
reader := bytes.NewReader(decodedBytes)
zr, err := zip.NewReader(reader, int64(len(decodedBytes)))
if err != nil {
return nil, err
}

return zr, nil
}

w.WriteHeader(http.StatusNotFound)
writeResponse(w, "no website found")
// Set the content type of the response based on the file's extension
func setContentType(w http.ResponseWriter, filePath string) {
// Set content type
switch {
case strings.HasSuffix(filePath, ".html"):
w.Header().Set("Content-Type", "text/html")
case strings.HasSuffix(filePath, ".css"):
w.Header().Set("Content-Type", "text/css")
case strings.HasSuffix(filePath, ".js"):
w.Header().Set("Content-Type", "text/javascript")
case strings.HasSuffix(filePath, ".png"):
w.Header().Set("Content-Type", "image/png")
case strings.HasSuffix(filePath, ".jpg"), strings.HasSuffix(filePath, ".jpeg"):
w.Header().Set("Content-Type", "image/jpeg")
case strings.HasSuffix(filePath, ".gif"):
w.Header().Set("Content-Type", "image/gif")
case strings.HasSuffix(filePath, ".svg"):
w.Header().Set("Content-Type", "image/svg+xml")
case strings.HasSuffix(filePath, ".ico"):
w.Header().Set("Content-Type", "image/x-icon")
default:
w.Header().Set("Content-Type", "text/plain")
}
}

// Helper function for writing response
func writeResponse(w http.ResponseWriter, resBody string) {
if _, err := w.Write([]byte(resBody)); err != nil {
fmt.Println("error writing response: ", err)
Expand Down
Binary file added app/webhost/cw_webhost.wasm
Binary file not shown.
13 changes: 13 additions & 0 deletions app/webhost/encode_zip_to_base64.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import base64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am good with a py script, but if you prefer bash is also possible

cat web.zip | base64 -w 0

import json

with open("web.zip", "rb") as f:
bytes = f.read()
encoded = base64.b64encode(bytes)

with open("web.zip.txt", "wb") as f:
f.write(encoded)

with open("new_web_cmd.txt", "w") as f:
wasm_execute_json = json.dumps({"new_website": {"name": "test", "source": encoded.decode("utf-8")}})
f.write(wasm_execute_json.replace(" ", ""))
Binary file added app/webhost/web.zip
Binary file not shown.
Loading