@ -0,0 +1 @@ |
|||||
|
A Sample Shopping Website (for training purposes) |
||||
@ -0,0 +1,8 @@ |
|||||
|
# You have to source this file from your shell ! |
||||
|
# |
||||
|
# . set-go-path.sh |
||||
|
# |
||||
|
|
||||
|
export GOPATH="$PWD" |
||||
|
echo "GOPATH=$GOPATH" |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
code.google.com |
||||
@ -0,0 +1,208 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"code.google.com/p/gorest" |
||||
|
"net/http" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
"io/ioutil" |
||||
|
"encoding/json" |
||||
|
) |
||||
|
|
||||
|
type Category struct { |
||||
|
Id string |
||||
|
Name string |
||||
|
} |
||||
|
|
||||
|
var Categories []Category = []Category { |
||||
|
{ "fringues", "Habillage" }, |
||||
|
{ "cuisine", "Cuisine" }, |
||||
|
{ "digital", "Digital" }, |
||||
|
{ "maison", "Bricolage" } } |
||||
|
|
||||
|
type Product struct { |
||||
|
Id int |
||||
|
Name string |
||||
|
Category string |
||||
|
Image string |
||||
|
Description string |
||||
|
Price float32 |
||||
|
Stock int |
||||
|
VendorId string |
||||
|
VendorName string |
||||
|
VendorProductId string |
||||
|
IsDigital bool |
||||
|
} |
||||
|
|
||||
|
type BuyResponse struct { |
||||
|
ResponseCode string |
||||
|
DownloadUrl string |
||||
|
} |
||||
|
|
||||
|
type CallbackResponse struct { |
||||
|
ResponseCode string `json:"code"` |
||||
|
RedirectUrl string `json:"redirect_url"` |
||||
|
} |
||||
|
|
||||
|
type BuyCallback struct { |
||||
|
VendorId string |
||||
|
VendorName string |
||||
|
VendorProductId string |
||||
|
} |
||||
|
|
||||
|
var Products []Product = []Product { |
||||
|
{ 0, "T-Shirt", "fringues", "brice.jpg", "Le T-Shirt de Brice de Nice.", 99.9, 1, "", "", "", false }, |
||||
|
{ 1, "Pull col roulé", "fringues", "pull.jpg", "Un pull à col roulé de couleur marron.", 2.00, 10, "", "", "", false }, |
||||
|
{ 2, "Cocotte minute", "cuisine", "cocotte.jpg", "La cocotte minute 'Presto'.", 45.00, 2, "", "", "", false }, |
||||
|
{ 3, "Marteau-Piqueur", "maison", "mp.jpg", "Le marteau piqueur 'DESTRUCTOR 2000'.", 600, 5, "", "", "", false } } |
||||
|
// { 4, "Visseuse Ultrasonique", "maison", "visseuse.jpg", "La visseuse-dévisseuse de chez Méga Store.", 600, 5, "mega-store", "Méga Store", "0001", false },
|
||||
|
// { 5, "DVD de Harry Poter", "digital", "dvd.jpg", "L'histoire de Ari l'empotteur au pays des merveilles.", 29.9, -1, "zouba-books", "Zouba Books", "12345", true } }
|
||||
|
|
||||
|
func main() { |
||||
|
gorest.RegisterService(new(MyShopService)) // Register our service
|
||||
|
http.Handle("/api/",gorest.Handle()) |
||||
|
http.Handle("/", http.FileServer(http.Dir("www-root"))) |
||||
|
http.ListenAndServe(":8787", nil) |
||||
|
} |
||||
|
|
||||
|
// REST Service Definition
|
||||
|
type MyShopService struct { |
||||
|
gorest.RestService `root:"/api/" consumes:"application/json" produces:"application/json"` |
||||
|
getCategories gorest.EndPoint `method:"GET" path:"/shop/category/" output:"[]Category"` |
||||
|
getProductsByCategory gorest.EndPoint `method:"GET" path:"/shop/product/?{category:string}" output:"[]Product"` |
||||
|
searchProducts gorest.EndPoint `method:"GET" path:"/shop/search/{criteria:string}" output:"[]Product"` |
||||
|
getProduct gorest.EndPoint `method:"GET" path:"/shop/product/{id:int}" output:"Product"` |
||||
|
addProduct gorest.EndPoint `method:"POST" path:"/market/product/" postdata:"Product"` |
||||
|
buyProduct gorest.EndPoint `method:"GET" path:"/shop/product/{id:int}/buy" output:"BuyResponse"` |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) SearchProducts(criteria string) []Product { |
||||
|
fmt.Println(">>> SearchProducts: criteria = ", criteria) |
||||
|
if (criteria == "") { |
||||
|
return Products |
||||
|
} |
||||
|
|
||||
|
sliceofcriteria := strings.Split(criteria, " ") |
||||
|
|
||||
|
FilteredProducts := []Product {} |
||||
|
for _, p := range Products { |
||||
|
var selected bool = false |
||||
|
name := strings.ToLower(p.Name) |
||||
|
desc := strings.ToLower(p.Description) |
||||
|
|
||||
|
for _, c := range sliceofcriteria { |
||||
|
c = strings.ToLower(c) |
||||
|
if strings.Contains(name, c) || strings.Contains(desc, c) { |
||||
|
selected = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if selected { |
||||
|
FilteredProducts = append(FilteredProducts, p) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return FilteredProducts |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) GetProducts() []Product { |
||||
|
fmt.Println(">>> GetProducts") |
||||
|
return Products |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) BuyProduct(id int) (resp BuyResponse) { |
||||
|
fmt.Println(">>> BuyProduct: id = ", id) |
||||
|
if id > len(Products) - 1 { |
||||
|
serv.ResponseBuilder().SetResponseCode(404).Overide(true) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if Products[id].Stock == 0 { |
||||
|
serv.ResponseBuilder().SetResponseCode(409).Overide(true) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if Products[id].Stock >0 { |
||||
|
Products[id].Stock-- |
||||
|
} |
||||
|
|
||||
|
return_code := "order-accepted" |
||||
|
redirect_url := "" |
||||
|
if Products[id].VendorId != "" { |
||||
|
elements := []string { "http://api.the-vendor.test:8080/api/vendor", Products[id].VendorId, "callback", Products[id].VendorProductId, "1" } |
||||
|
callback := strings.Join(elements, "/") |
||||
|
fmt.Println(">>> BuyProduct: firing callback to vendor", Products[id].VendorName, "with URL =", callback) |
||||
|
|
||||
|
client := &http.Client{} |
||||
|
req, err := http.NewRequest("GET", callback, nil) |
||||
|
resp, err := client.Do(req) |
||||
|
if err != nil { |
||||
|
fmt.Println(">>> BuyProduct: ERROR", err) |
||||
|
return_code = "error" |
||||
|
serv.ResponseBuilder().SetResponseCode(500).Overide(true) |
||||
|
} else { |
||||
|
defer resp.Body.Close() |
||||
|
fmt.Println(">>> BuyProduct: response Status:", resp.Status) |
||||
|
if resp.Status != "200 OK" { |
||||
|
body, _ := ioutil.ReadAll(resp.Body) |
||||
|
fmt.Println(">>> BuyProduct: response Body:", string(body)) |
||||
|
return_code = "error" |
||||
|
serv.ResponseBuilder().SetResponseCode(500).Overide(true) |
||||
|
} else { |
||||
|
if Products[id].IsDigital { |
||||
|
fmt.Println(">>> BuyProduct: Decoding JSON response") |
||||
|
json_resp := new(CallbackResponse) |
||||
|
json.NewDecoder(resp.Body).Decode(json_resp) |
||||
|
redirect_url = json_resp.RedirectUrl; |
||||
|
} else { |
||||
|
fmt.Println(">>> BuyProduct: Ignoring JSON response") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fmt.Println(">>> BuyProduct: return_code =", return_code, "redirect_url =", redirect_url) |
||||
|
|
||||
|
resp = BuyResponse { return_code, redirect_url } |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func(serv MyShopService) GetProduct(id int) (p Product) { |
||||
|
fmt.Println(">>> GetProduct: id =", id) |
||||
|
if id > len(Products) - 1 { |
||||
|
serv.ResponseBuilder().SetResponseCode(404).Overide(true) |
||||
|
return |
||||
|
} |
||||
|
p = Products[id] |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) AddProduct(posted Product) { |
||||
|
fmt.Println(">>> AddProduct: posted =", posted) |
||||
|
id := len(Products) |
||||
|
posted.Id = id |
||||
|
Products = append(Products, posted); |
||||
|
serv.ResponseBuilder().Created("/api/shop/product/"+string(id)) |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) GetProductsByCategory(category string) []Product { |
||||
|
fmt.Println(">>> GetProductsByCategory: category =", category) |
||||
|
if (category == "") { |
||||
|
return Products |
||||
|
} |
||||
|
|
||||
|
FilteredProducts := []Product {} |
||||
|
for _, p := range Products { |
||||
|
if p.Category == category { |
||||
|
FilteredProducts = append(FilteredProducts, p) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return FilteredProducts |
||||
|
} |
||||
|
|
||||
|
func(serv MyShopService) GetCategories() []Category { |
||||
|
fmt.Println(">>> GetCategories") |
||||
|
return Categories |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
curl -H "Content-Type: application/json" \ |
||||
|
-d '{ "Name": "A New Product", "Category": "maison", "Image": "logo.png", "Description": "My brand new product", "Price": 1, "Stock": 1 }' \ |
||||
|
-X POST \ |
||||
|
-D - \ |
||||
|
http://localhost:8787/api/shop/product/ |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
.DS_Store |
||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,31 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>My Shop</title> |
||||
|
<script type="text/javascript" src="js/dojo/dojo.js"></script> |
||||
|
<script type="text/javascript" src="js/shop.js"></script> |
||||
|
<link rel="stylesheet" type="text/css" href="style.css"> |
||||
|
<link rel="stylesheet" href="js/dijit/themes/claro/claro.css" /> |
||||
|
</head> |
||||
|
<body class="claro"> |
||||
|
<div id="main_pane"> |
||||
|
<div id="site_header"> |
||||
|
<div id="site_logo"><img src="/img/logo.png" height="75px"/></div> |
||||
|
</div> |
||||
|
<div id="pages_container"> |
||||
|
<div style="position: relative;"> |
||||
|
<div id="page_title"> |
||||
|
<h2><span id="title">Boutique</span></h2> |
||||
|
<h5><span id="subtitle">Subtitle</span></h5> |
||||
|
</div> |
||||
|
|
||||
|
<div id="content_pane"> |
||||
|
<div id="search_bar">Recherche : <input type="text" id="search_textbox" /><div id="search_results"></div></div> |
||||
|
<div id="category_bar"><div id="category_list_placeholder"></div></div> |
||||
|
<div id="products_pane"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,3 @@ |
|||||
|
dojo |
||||
|
dojox |
||||
|
dijit |
||||
@ -0,0 +1,217 @@ |
|||||
|
var apibase = "/api/shop"; |
||||
|
var dialog; |
||||
|
|
||||
|
require([ "dojo/ready", "dojo/request/xhr", "dojo/dom-construct", "dojo/dom", "dojo/on", "dojo/dom-style", "dojo/dom-attr", "dijit/Dialog" ], |
||||
|
function(ready, xhr, domConstruct, dom, on, domStyle, domAttr, Dialog) { |
||||
|
|
||||
|
function setOnCategoryClickHandler(node, catid, catname) { |
||||
|
on(node, "click", function() { |
||||
|
loadProducts(catid); |
||||
|
setSubTitle(catname); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function setOnProductClickHandler(node, id) { |
||||
|
on(node, "click", function() { |
||||
|
displayProduct(id); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function setSearchHandler() { |
||||
|
on(dom.byId("search_textbox"), "keyup", doSearch); |
||||
|
on(dom.byId("search_textbox"), "blur", function () { |
||||
|
console.log("SEARCH TEXT BOX >> Blur"); |
||||
|
window.setInterval(function () { console.log("SEARCH TEXT BOX >> Je la cache"); domStyle.set("search_results", "visibility", "hidden"); }, 100); |
||||
|
}); |
||||
|
on(dom.byId("search_textbox"), "focus", function () { |
||||
|
console.log("SEARCH TEXT BOX >> Focus"); |
||||
|
var searchCriteria = domAttr.get("search_textbox", "value"); |
||||
|
if (searchCriteria != "") { |
||||
|
domStyle.set("search_results", "visibility", "visible"); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function setBuyHandler(node, id) { |
||||
|
on(node, "click", function() { |
||||
|
buyProduct(id); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function buyProduct(id) { |
||||
|
xhr(apibase + "/product/" + encodeURI(id) + "/buy", |
||||
|
{ handleAs: "json" } |
||||
|
).then(function (data) { |
||||
|
dialog.set("title", "Commande acceptée"); |
||||
|
dialog.set("content", "La commande est partie. Vous allez très prochainement recevoir le produit."); |
||||
|
console.log("test..."); |
||||
|
if (data.DownloadUrl != null && data.DownloadUrl != "") { |
||||
|
console.log("popup !"); |
||||
|
window.location.href = data.DownloadUrl; |
||||
|
} |
||||
|
dialog.show(); |
||||
|
displayProduct(id); // Refresh UI
|
||||
|
}, function (err) { |
||||
|
if (err.response != null && err.response.status == 409) { |
||||
|
dialog.set("title", "Erreur"); |
||||
|
dialog.set("content", "Le produit n'est plus en stock. Désolé."); |
||||
|
dialog.show(); |
||||
|
} else { |
||||
|
dialog.set("title", "OOPS"); |
||||
|
dialog.set("content", "Erreur interne. Désolé."); |
||||
|
dialog.show(); |
||||
|
} |
||||
|
console.log(err); |
||||
|
displayProduct(id); // Refresh UI
|
||||
|
}, function (evt) { |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function doSearch(evt) { |
||||
|
var searchCriteria = domAttr.get("search_textbox", "value"); |
||||
|
if (searchCriteria == "") { |
||||
|
domStyle.set("search_results", "visibility", "hidden"); |
||||
|
return; |
||||
|
} else { |
||||
|
domStyle.set("search_results", "visibility", "visible"); |
||||
|
} |
||||
|
|
||||
|
xhr(apibase + "/search/" + encodeURI(searchCriteria), |
||||
|
{ handleAs: "json" } |
||||
|
).then(function (data) { |
||||
|
domConstruct.empty("search_results"); |
||||
|
var placeholder = dom.byId("search_results"); |
||||
|
for (var i = 0; i < data.length; i++) { |
||||
|
var div = domConstruct.create("div", {}, placeholder); |
||||
|
domConstruct.create("img", { width: "32px", src: "/img/" + data[i].Image }, div); |
||||
|
domConstruct.create("span", { textContent: data[i].Name }, div); |
||||
|
setOnProductClickHandler(div, data[i].Id); |
||||
|
} |
||||
|
if (data.length == 0) { |
||||
|
domConstruct.create("span", { textContent: "Aucun résultat", 'class': "no_result" }, placeholder); |
||||
|
} |
||||
|
}, function (err) { |
||||
|
console.log(err); |
||||
|
}, function (evt) { |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function displayProduct(id) { |
||||
|
xhr(apibase + "/product/" + encodeURI(id), |
||||
|
{ handleAs: "json" } |
||||
|
).then(function (data) { |
||||
|
domConstruct.empty("products_pane"); |
||||
|
var placeholder = dom.byId("products_pane"); |
||||
|
var div = dojo.create("div", { 'class': 'product_detail' }, placeholder); |
||||
|
dojo.create("h1", { 'class': "product_name", textContent: data.Name }, div); |
||||
|
var div2 = dojo.create("div", {}, div) |
||||
|
dojo.create("img", { src: "/img/" + data.Image, height: "200px" }, div2); |
||||
|
dojo.create("span", { 'class': "product_price", textContent: data.Price + " €" }, div2); |
||||
|
if (data.Stock != "-1") { |
||||
|
dojo.create("span", { 'class': "product_stock", textContent: "En Stock: " + data.Stock }, div2); |
||||
|
} |
||||
|
if (data.VendorName != "") { |
||||
|
dojo.create("span", { 'class': "sold_by", textContent: "Vendu par: " + data.VendorName }, div2); |
||||
|
} |
||||
|
|
||||
|
var buy_button = dojo.create("div", { 'class': "buy_button" }, div2); |
||||
|
dojo.create("div", { textContent: "Acheter !" }, buy_button); |
||||
|
setBuyHandler(buy_button, data.Id); |
||||
|
|
||||
|
dojo.create("div", { 'class': "product_description", textContent: data.Description }, div); |
||||
|
}, function (err) { |
||||
|
if (err.response != null && err.response.status == 404) { |
||||
|
dialog.set("title", "Erreur"); |
||||
|
dialog.set("content", "Le produit a été retiré de la vente. Désolé."); |
||||
|
dialog.show(); |
||||
|
} else { |
||||
|
dialog.set("title", "OOPS"); |
||||
|
dialog.set("content", "Erreur interne. Désolé."); |
||||
|
dialog.show(); |
||||
|
} |
||||
|
console.log(err); |
||||
|
}, function (evt) { |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function setSubTitle(name) { |
||||
|
domConstruct.empty("subtitle"); |
||||
|
dom.byId("subtitle").textContent = name; |
||||
|
} |
||||
|
|
||||
|
function loadProducts(catid) { |
||||
|
var queryString = ""; |
||||
|
if (catid != null) { |
||||
|
queryString = "?category=" + encodeURI(catid); |
||||
|
} |
||||
|
xhr(apibase + "/product/" + queryString, |
||||
|
{ handleAs: "json" } |
||||
|
).then(function (data) { |
||||
|
domConstruct.empty("products_pane"); |
||||
|
var placeholder = dom.byId("products_pane"); |
||||
|
var table = domConstruct.create("table", { 'class': "product_table" }, placeholder); |
||||
|
var current_tr = null; |
||||
|
var i = 0; |
||||
|
for (; i < data.length; i++) { |
||||
|
if (i % 3 == 0) { |
||||
|
current_tr = domConstruct.create("tr", {}, table); |
||||
|
} |
||||
|
var td = domConstruct.create("td", {}, current_tr); |
||||
|
domConstruct.create("img", { width: "100px", src: "/img/" + data[i].Image }, td); |
||||
|
domConstruct.create("span", { textContent: data[i].Name }, td); |
||||
|
setOnProductClickHandler(td, data[i].Id); |
||||
|
} |
||||
|
// Fill remaining columns if less than 3 products
|
||||
|
for (; i < 3; i++) { |
||||
|
domConstruct.create("td", {}, current_tr); |
||||
|
} |
||||
|
}, function (err) { |
||||
|
dialog.set("title", "OOPS"); |
||||
|
dialog.set("content", "Erreur interne. Désolé."); |
||||
|
dialog.show(); |
||||
|
console.log(err); |
||||
|
}, function (evt) { |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function loadCategories() { |
||||
|
xhr(apibase + "/category/", |
||||
|
{ handleAs: "json" } |
||||
|
).then(function (data) { |
||||
|
var placeholder = dom.byId("category_list_placeholder"); |
||||
|
domConstruct.empty("category_list_placeholder"); |
||||
|
var all_products_node = domConstruct.create("span", { textContent: "Tous les produits", 'class': "category_item" }, placeholder); |
||||
|
setOnCategoryClickHandler(all_products_node, null, "Tous les produits"); |
||||
|
for (var i = 0; i < data.length; i++) { |
||||
|
var catid = data[i].Id; |
||||
|
var catname = data[i].Name; |
||||
|
var node = domConstruct.create("span", { textContent: data[i].Name, 'class': "category_item" }, placeholder); |
||||
|
setOnCategoryClickHandler(node, catid, catname); |
||||
|
} |
||||
|
}, function (err) { |
||||
|
dialog.set("title", "OOPS"); |
||||
|
dialog.set("content", "Erreur interne. Désolé."); |
||||
|
dialog.show(); |
||||
|
console.log(err); |
||||
|
}, function (evt) { |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ready(function() { |
||||
|
loadCategories(); |
||||
|
loadProducts(null); |
||||
|
setSubTitle("Tous les produits"); |
||||
|
setSearchHandler(); |
||||
|
dialog = new Dialog({ |
||||
|
id: "global_dialog", |
||||
|
title: "...", |
||||
|
content: "...", |
||||
|
style: "width: 500px; display: none;" |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,218 @@ |
|||||
|
#search_bar { |
||||
|
position: absolute; |
||||
|
top: 0px; |
||||
|
right: 0px; |
||||
|
width: 450px; |
||||
|
height: 50px; |
||||
|
} |
||||
|
|
||||
|
#search_textbox { |
||||
|
width: 350px; |
||||
|
height: 20px; |
||||
|
position: absolute; |
||||
|
top: 0px; |
||||
|
right: 0px; |
||||
|
background-color: gray; |
||||
|
} |
||||
|
|
||||
|
#search_results { |
||||
|
width: 350px; |
||||
|
position: absolute; |
||||
|
top: 30px; |
||||
|
right: 0px; |
||||
|
background-color: white; |
||||
|
border: 1px solid lightgrey; |
||||
|
z-index: 255; |
||||
|
visibility: hidden; |
||||
|
} |
||||
|
|
||||
|
#search_results span, #search_results img { |
||||
|
display:inline-block; |
||||
|
vertical-align:middle; |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
|
||||
|
#search_results div { |
||||
|
cursor: pointer; cursor: hand; |
||||
|
border: 1px solid white; |
||||
|
} |
||||
|
|
||||
|
#search_results div:hover { |
||||
|
border: 1px solid gray; |
||||
|
} |
||||
|
|
||||
|
.no_result { |
||||
|
color: lightgrey; |
||||
|
font-style: italic; |
||||
|
} |
||||
|
|
||||
|
#category_bar { |
||||
|
position: absolute; |
||||
|
top: 100px; |
||||
|
left: 0px; |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
#products_pane { |
||||
|
position: absolute; |
||||
|
top: 100px; |
||||
|
left: 250px; |
||||
|
right: 0px; |
||||
|
min-height: 300px; |
||||
|
} |
||||
|
|
||||
|
#content_pane { |
||||
|
position: absolute; |
||||
|
top: 0px; |
||||
|
left: 30px; |
||||
|
right: 30px; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
background-color: #AAAAAA; |
||||
|
font-family: Sans-Serif; |
||||
|
} |
||||
|
|
||||
|
#main_pane { |
||||
|
position: relative; |
||||
|
width: 80%; |
||||
|
margin: auto; |
||||
|
background-color: #EEEEEE; |
||||
|
min-height: 800px; |
||||
|
} |
||||
|
|
||||
|
#site_header { |
||||
|
position: absolute; |
||||
|
top: 0px; |
||||
|
height: 116px; |
||||
|
left: 30px; |
||||
|
right: 30px; |
||||
|
} |
||||
|
|
||||
|
#site_logo { |
||||
|
position: absolute; |
||||
|
top: 40px; |
||||
|
width: 345px; |
||||
|
left: 40px; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
#pages_container { |
||||
|
position: absolute; |
||||
|
top: 140px; |
||||
|
bottom: 30px; |
||||
|
right: 30px; |
||||
|
left: 60px; |
||||
|
} |
||||
|
|
||||
|
#page_title { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
right: 30px; |
||||
|
left: 30px; |
||||
|
font: normal normal normal 25px/1.2em sans-serif; |
||||
|
} |
||||
|
|
||||
|
#page_title > h2 { |
||||
|
line-height: 1.1em; |
||||
|
font: normal normal normal 22px/1.1em sans-serif; |
||||
|
color: #00CCFF; |
||||
|
margin : 0; |
||||
|
padding : 0; |
||||
|
border : 0; |
||||
|
outline : 0; |
||||
|
} |
||||
|
|
||||
|
#page_title > h5 { |
||||
|
color: #7F7F7F; |
||||
|
line-height: 1.2em; |
||||
|
letter-spacing: normal; |
||||
|
margin : 0; |
||||
|
padding : 0; |
||||
|
border : 0; |
||||
|
outline : 0; |
||||
|
} |
||||
|
|
||||
|
.product_table { |
||||
|
width: 100%; |
||||
|
border-spacing: 10px; |
||||
|
border-collapse: separate; |
||||
|
} |
||||
|
|
||||
|
.product_table span, .product_table img { |
||||
|
display: block; |
||||
|
margin: auto; |
||||
|
} |
||||
|
|
||||
|
.product_table img { |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.product_table td { |
||||
|
padding-top: 10px; |
||||
|
padding-bottom: 10px; |
||||
|
text-align: center; |
||||
|
background-color: white; |
||||
|
width: 33%; |
||||
|
color: darkgrey; |
||||
|
cursor: pointer; cursor: hand; |
||||
|
} |
||||
|
|
||||
|
.category_item { |
||||
|
display: block; |
||||
|
margin-bottom: 10px; |
||||
|
text-decoration: none; |
||||
|
cursor: pointer; cursor: hand; |
||||
|
} |
||||
|
|
||||
|
.category_item:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
.product_price { |
||||
|
position: absolute; |
||||
|
top: 20px; |
||||
|
left: 300px; |
||||
|
color: red; |
||||
|
font-size: x-large; |
||||
|
} |
||||
|
|
||||
|
.product_detail > div { |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.product_stock { |
||||
|
position: absolute; |
||||
|
top: 60px; |
||||
|
left: 300px; |
||||
|
color: darkgreen; |
||||
|
} |
||||
|
|
||||
|
.sold_by { |
||||
|
position: absolute; |
||||
|
top: 80px; |
||||
|
left: 300px; |
||||
|
color: darkgreen; |
||||
|
} |
||||
|
|
||||
|
.product_description { |
||||
|
margin-top: 20px; |
||||
|
color: darkgrey; |
||||
|
} |
||||
|
|
||||
|
.buy_button { |
||||
|
position: absolute; |
||||
|
top: 150px; |
||||
|
left: 300px; |
||||
|
background-color: red; |
||||
|
color: white; |
||||
|
width: 100px; |
||||
|
height: 30px; |
||||
|
cursor: pointer; cursor: hand; |
||||
|
} |
||||
|
|
||||
|
.buy_button > div { |
||||
|
text-align: center; |
||||
|
line-height: 30px; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
#!/bin/sh |
||||
|
|
||||
|
(cd src && javac fr/itix/soapbackend/*.java) |
||||
|
|
||||
|
BASE=tomcat/webapps/axis2/WEB-INF/services/ |
||||
|
NAME=VendorBackend |
||||
|
mkdir -p build/$NAME/META-INF build/$NAME/fr/itix/soapbackend/ |
||||
|
cp src/fr/itix/soapbackend/*.class build/$NAME/fr/itix/soapbackend/ |
||||
|
cp src/services.xml build/$NAME/META-INF/ |
||||
|
|
||||
|
cp -rv build/$NAME $BASE/ |
||||
|
|
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
*.class |
||||
@ -0,0 +1,14 @@ |
|||||
|
package fr.itix.soapbackend; |
||||
|
|
||||
|
public class VendorBackend { |
||||
|
public String NotifySale(String productId, int number, String callerID) { |
||||
|
if ("12345".equals(productId) && "zouba-books".equals(callerID)) { |
||||
|
return "OK;http://online-shop.zouba-books.test:8787/book-1234.pdf"; |
||||
|
} |
||||
|
|
||||
|
if ("0001".equals(productId) && "mega-store".equals(callerID)) { |
||||
|
return "OK;"; |
||||
|
} |
||||
|
throw new RuntimeException("Unknown caller id or wrong product id"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
<service name="VendorBackend" scope="application"> |
||||
|
<description> |
||||
|
Vendor Backend |
||||
|
</description> |
||||
|
<messageReceivers> |
||||
|
<messageReceiver |
||||
|
mep="http://www.w3.org/2004/08/wsdl/in-only" |
||||
|
class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver"/> |
||||
|
<messageReceiver |
||||
|
mep="http://www.w3.org/2004/08/wsdl/in-out" |
||||
|
class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/> |
||||
|
</messageReceivers> |
||||
|
<parameter name="ServiceClass"> |
||||
|
fr.itix.soapbackend.VendorBackend |
||||
|
</parameter> |
||||
|
</service> |
||||
@ -0,0 +1,22 @@ |
|||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015 Nicolas MASSE |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
|
|
||||
@ -0,0 +1,12 @@ |
|||||
|
## WebAPI-Samples |
||||
|
Sample Code for various Web API Protocols (SOAP, REST, etc.) |
||||
|
|
||||
|
### SOAP samples |
||||
|
- [MTOM Attachments in Java](./SOAP/MTOM/) |
||||
|
|
||||
|
### REST samples |
||||
|
- TODO |
||||
|
|
||||
|
### Utils |
||||
|
- [A Reverse Proxy written in GO](./Utils/ReverseProxy/) |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
tomcat |
||||
@ -0,0 +1,29 @@ |
|||||
|
# MTOM Sample Server Code |
||||
|
|
||||
|
## Introduction to MTOM |
||||
|
|
||||
|
MTOM is a standard to attach a file to a SOAP message (others way to do so are: MIME Attachments or in-line base64). |
||||
|
|
||||
|
For a good introduction to MTOM read : |
||||
|
- http://www.mkyong.com/webservices/jax-ws/jax-ws-attachment-with-mtom/ |
||||
|
- https://axis.apache.org/axis2/java/core/docs/mtom-guide.html |
||||
|
- http://stackoverflow.com/questions/215741/how-does-mtom-work |
||||
|
|
||||
|
### How to know when a SOAP Message use MTOM ? |
||||
|
|
||||
|
The easy way : |
||||
|
- The HTTP request is a mime multipart |
||||
|
``` |
||||
|
POST /path/to/ws HTTP/1.1 |
||||
|
Content-type: multipart/related; start="bla"; type="application/xop+xml"; boundary="uuid:bla..."; |
||||
|
``` |
||||
|
- The SOAP Payload contains `Include` elements in the namespace `http://www.w3.org/2004/08/xop/include` |
||||
|
``` |
||||
|
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" |
||||
|
href="bla bla bla"> |
||||
|
``` |
||||
|
|
||||
|
## MTOM Server Code Setup |
||||
|
|
||||
|
See [the documentation](./doc/README.md). |
||||
|
|
||||
@ -0,0 +1,14 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
NAME=SoapBackend |
||||
|
|
||||
|
function die() { |
||||
|
echo "ERROR: $@" |
||||
|
exit 1 |
||||
|
} |
||||
|
|
||||
|
cd src || die "No source code" |
||||
|
javac $(find . -iname *.java) || die "Compilation error" |
||||
|
jar cvf ../build/$NAME.aar $(find . -iname *.class -or -iname *.xml) || die "Cannot build jar" |
||||
|
|
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
*.aar |
||||
@ -0,0 +1,104 @@ |
|||||
|
## MTOM Server Code Setup |
||||
|
|
||||
|
### Pre-requisites |
||||
|
|
||||
|
To make this sample code work, you need: |
||||
|
- Tomcat (tested against version 7) |
||||
|
- Axis2 (tested against version 1.6) |
||||
|
|
||||
|
Get tomcat7 and install it in a `tomcat` folder. |
||||
|
``` |
||||
|
$ wget http://www.eu.apache.org/dist/tomcat/tomcat-7/v7.0.62/bin/apache-tomcat-7.0.62.tar.gz && tar zxvf apache-tomcat-*.tar.gz && mv apache-tomcat-*/ tomcat |
||||
|
``` |
||||
|
|
||||
|
Get axis2 and install the axis2.jar in `tomcat/webapps/axis2` |
||||
|
|
||||
|
``` |
||||
|
$ cd tomcat/webapps |
||||
|
$ wget http://www.eu.apache.org/dist//axis/axis2/java/core/1.6.2/axis2-1.6.2-war.zip && unzip axis2-1.6.2-war.zip axis2.war |
||||
|
$ cd .. |
||||
|
$ ./bin/startup.sh |
||||
|
``` |
||||
|
|
||||
|
### Build |
||||
|
|
||||
|
Build the .aar archive and deploy it. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Change the Axis Configuration to enable MTOM |
||||
|
|
||||
|
Edit the Axis2 configuration file: `tomcat/webapps/axis2/WEB-INF/conf/axis2.xml` and set the `enableMTOM` parameter to `true`. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
**DO NOT FORGET TO RESTART TOMCAT !!!** |
||||
|
|
||||
|
## Test the MTOM attachment |
||||
|
|
||||
|
### Retrieve the WSDL |
||||
|
|
||||
|
Make sure Tomcat is started and the .aar archive is deployed. Then go to http://localhost:8080/axis2/services/listServices. |
||||
|
|
||||
|
Click on the `MTOMService` to get the WSDL. |
||||
|
|
||||
|
*Note:* make sure you save the file in its original format (File > Save As...). **Do not use copy paste that may break the XML format.** |
||||
|
|
||||
|
Just in case, you can find a copy of the WSDL [here](../wsdl/MtomService.wsdl). |
||||
|
|
||||
|
|
||||
|
### Test in SOAP UI |
||||
|
|
||||
|
1. Get SOAP UI (tested with version 5) |
||||
|
2. Import the WSDL |
||||
|
3. Open the auto-generated request for the `MtomServiceSoap12Binding` binding |
||||
|
 |
||||
|
4. Make sure `Enable MTOM` and `Force MTOM` in the bottom left pane are set to `true`. |
||||
|
 |
||||
|
5. Import your attachment in the bottom pane. |
||||
|
 |
||||
|
6. Make sure to reference the attachment in your SOAP message using `cid:attachment-name`. |
||||
|
 |
||||
|
7. Make sure the attachement part in the bottom pane is set and the attachment type is set to `XOP` |
||||
|
 |
||||
|
8. Fire the request and observe the result ! |
||||
|
 |
||||
|
|
||||
|
Note: by clicking on the raw button in the request pane, you can have a look at the complete HTTP request. |
||||
|
|
||||
|
``` |
||||
|
POST http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap12Endpoint/ HTTP/1.1 |
||||
|
Accept-Encoding: gzip,deflate |
||||
|
Content-Type: multipart/related; type="application/xop+xml"; start="<rootpart@soapui.org>"; start-info="application/soap+xml"; action="urn:countBytes"; boundary="----=_Part_1_223849750.1433937520698" |
||||
|
MIME-Version: 1.0 |
||||
|
Content-Length: 912 |
||||
|
Host: localhost:8080 |
||||
|
Connection: Keep-Alive |
||||
|
User-Agent: Apache-HttpClient/4.1.1 (java 1.5) |
||||
|
|
||||
|
|
||||
|
------=_Part_1_223849750.1433937520698 |
||||
|
Content-Type: application/xop+xml; charset=UTF-8; type="application/soap+xml"; action="countBytes" |
||||
|
Content-Transfer-Encoding: 8bit |
||||
|
Content-ID: <rootpart@soapui.org> |
||||
|
|
||||
|
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:mtom="http://itix.fr/soap/mtom"> |
||||
|
<soap:Header/> |
||||
|
<soap:Body> |
||||
|
<mtom:countBytes> |
||||
|
<!--Optional:--> |
||||
|
<mtom:args0><inc:Include href="cid:my-attachment.txt" xmlns:inc="http://www.w3.org/2004/08/xop/include"/></mtom:args0> |
||||
|
</mtom:countBytes> |
||||
|
</soap:Body> |
||||
|
</soap:Envelope> |
||||
|
------=_Part_1_223849750.1433937520698 |
||||
|
Content-Type: text/plain; charset=us-ascii; name=my-attachment.txt |
||||
|
Content-Transfer-Encoding: 7bit |
||||
|
Content-ID: <my-attachment.txt> |
||||
|
Content-Disposition: attachment; name="my-attachment.txt"; filename="my-attachment.txt" |
||||
|
|
||||
|
Hello World ! |
||||
|
|
||||
|
------=_Part_1_223849750.1433937520698-- |
||||
|
``` |
||||
|
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 52 KiB |
@ -0,0 +1,16 @@ |
|||||
|
<service name="VendorBackend" scope="application"> |
||||
|
<description> |
||||
|
SOAP Backend that exposes an MTOM enabled service |
||||
|
</description> |
||||
|
<messageReceivers> |
||||
|
<messageReceiver |
||||
|
mep="http://www.w3.org/2004/08/wsdl/in-only" |
||||
|
class="org.apache.axis2.rpc.receivers.RPCInOnlyMessageReceiver"/> |
||||
|
<messageReceiver |
||||
|
mep="http://www.w3.org/2004/08/wsdl/in-out" |
||||
|
class="org.apache.axis2.rpc.receivers.RPCMessageReceiver"/> |
||||
|
</messageReceivers> |
||||
|
<parameter name="ServiceClass"> |
||||
|
fr.itix.soapbackend.MTOMService |
||||
|
</parameter> |
||||
|
</service> |
||||
@ -0,0 +1 @@ |
|||||
|
*.class |
||||
@ -0,0 +1,16 @@ |
|||||
|
package fr.itix.soapbackend; |
||||
|
|
||||
|
import javax.jws.WebMethod; |
||||
|
import javax.jws.WebService; |
||||
|
import javax.xml.ws.soap.MTOM; |
||||
|
|
||||
|
@MTOM |
||||
|
@WebService(name="MtomPortType", |
||||
|
serviceName="MtomService", |
||||
|
targetNamespace="http://itix.fr/soap/mtom") |
||||
|
public class MTOMService { |
||||
|
@WebMethod |
||||
|
public int countBytes(byte[] bytes) { |
||||
|
return bytes.length; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://itix.fr/soap/mtom" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:ns1="http://org.apache.axis2/xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://itix.fr/soap/mtom"> |
||||
|
<wsdl:documentation>VendorBackend</wsdl:documentation> |
||||
|
<wsdl:types> |
||||
|
<xs:schema attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://itix.fr/soap/mtom"> |
||||
|
<xs:element name="countBytes"> |
||||
|
<xs:complexType> |
||||
|
<xs:sequence> |
||||
|
<xs:element minOccurs="0" name="args0" nillable="true" type="xs:base64Binary"/> |
||||
|
</xs:sequence> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
<xs:element name="countBytesResponse"> |
||||
|
<xs:complexType> |
||||
|
<xs:sequence> |
||||
|
<xs:element minOccurs="0" name="return" type="xs:int"/> |
||||
|
</xs:sequence> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
|
</wsdl:types> |
||||
|
<wsdl:message name="countBytesRequest"> |
||||
|
<wsdl:part name="parameters" element="ns:countBytes"/> |
||||
|
</wsdl:message> |
||||
|
<wsdl:message name="countBytesResponse"> |
||||
|
<wsdl:part name="parameters" element="ns:countBytesResponse"/> |
||||
|
</wsdl:message> |
||||
|
<wsdl:portType name="MtomServicePortType"> |
||||
|
<wsdl:operation name="countBytes"> |
||||
|
<wsdl:input message="ns:countBytesRequest" wsaw:Action="urn:countBytes"/> |
||||
|
<wsdl:output message="ns:countBytesResponse" wsaw:Action="urn:countBytesResponse"/> |
||||
|
</wsdl:operation> |
||||
|
</wsdl:portType> |
||||
|
<wsdl:binding name="MtomServiceSoap11Binding" type="ns:MtomServicePortType"> |
||||
|
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> |
||||
|
<wsdl:operation name="countBytes"> |
||||
|
<soap:operation soapAction="urn:countBytes" style="document"/> |
||||
|
<wsdl:input> |
||||
|
<soap:body use="literal"/> |
||||
|
</wsdl:input> |
||||
|
<wsdl:output> |
||||
|
<soap:body use="literal"/> |
||||
|
</wsdl:output> |
||||
|
</wsdl:operation> |
||||
|
</wsdl:binding> |
||||
|
<wsdl:binding name="MtomServiceSoap12Binding" type="ns:MtomServicePortType"> |
||||
|
<soap12:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> |
||||
|
<wsdl:operation name="countBytes"> |
||||
|
<soap12:operation soapAction="urn:countBytes" style="document"/> |
||||
|
<wsdl:input> |
||||
|
<soap12:body use="literal"/> |
||||
|
</wsdl:input> |
||||
|
<wsdl:output> |
||||
|
<soap12:body use="literal"/> |
||||
|
</wsdl:output> |
||||
|
</wsdl:operation> |
||||
|
</wsdl:binding> |
||||
|
<wsdl:binding name="MtomServiceHttpBinding" type="ns:MtomServicePortType"> |
||||
|
<http:binding verb="POST"/> |
||||
|
<wsdl:operation name="countBytes"> |
||||
|
<http:operation location="countBytes"/> |
||||
|
<wsdl:input> |
||||
|
<mime:content type="application/xml" part="parameters"/> |
||||
|
</wsdl:input> |
||||
|
<wsdl:output> |
||||
|
<mime:content type="application/xml" part="parameters"/> |
||||
|
</wsdl:output> |
||||
|
</wsdl:operation> |
||||
|
</wsdl:binding> |
||||
|
<wsdl:service name="MtomService"> |
||||
|
<wsdl:port name="MtomServiceHttpSoap11Endpoint" binding="ns:MtomServiceSoap11Binding"> |
||||
|
<soap:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap11Endpoint/"/> |
||||
|
</wsdl:port> |
||||
|
<wsdl:port name="MtomServiceHttpSoap12Endpoint" binding="ns:MtomServiceSoap12Binding"> |
||||
|
<soap12:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpSoap12Endpoint/"/> |
||||
|
</wsdl:port> |
||||
|
<wsdl:port name="MtomServiceHttpEndpoint" binding="ns:MtomServiceHttpBinding"> |
||||
|
<http:address location="http://localhost:8080/axis2/services/MtomService.MtomServiceHttpEndpoint/"/> |
||||
|
</wsdl:port> |
||||
|
</wsdl:service> |
||||
|
</wsdl:definitions> |
||||
@ -0,0 +1 @@ |
|||||
|
This a sample WSDL, generated by Axis |
||||
@ -0,0 +1,27 @@ |
|||||
|
## Reverse Proxy written in GO |
||||
|
This reverse proxy listens on a local port and forward all requests to a named host. It honors the proxy environment variables. |
||||
|
|
||||
|
### Initial Need |
||||
|
|
||||
|
Once upon a time, I had to circumvent a bug in a product that could not handle correctly an HTTPS connection to a proxy. |
||||
|
|
||||
|
Since it was an HTTPS connection, I could not setup a transparent proxy. |
||||
|
|
||||
|
### What it does |
||||
|
|
||||
|
It opens a local port and listen to HTTP requests, forwards the requests to a named host and send back the response. |
||||
|
|
||||
|
### How to use it |
||||
|
|
||||
|
```bash |
||||
|
go run src/itix.fr/forward/main.go -local-port 8080 -target https://www.opentrust.com |
||||
|
curl -D - http://localhost:8080/robots.txt |
||||
|
``` |
||||
|
|
||||
|
If you want to go through a proxy, do not forget to set the ```http_proxy``` and ```https_proxy``` variables ! |
||||
|
|
||||
|
```bash |
||||
|
export http_proxy=http://my.proxy:8888/ |
||||
|
export https_proxy=http://my.proxy:8888/ |
||||
|
``` |
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
export GOPATH="$PWD" |
||||
|
echo "GOPATH=$GOPATH" |
||||
|
|
||||
@ -0,0 +1,96 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"fmt" |
||||
|
"flag" |
||||
|
"net/http" |
||||
|
"net/http/httputil" |
||||
|
"net/url" |
||||
|
) |
||||
|
|
||||
|
// The local port on which we should listen to
|
||||
|
var local_port int |
||||
|
|
||||
|
// The target URL on which we should redirect requests
|
||||
|
var target string |
||||
|
|
||||
|
func init() { |
||||
|
// --local-port=9009
|
||||
|
flag.IntVar(&local_port, "local-port", 9090, "the TCP port to listen to") |
||||
|
|
||||
|
// --target=http://www.perdu.com
|
||||
|
flag.StringVar(&target, "target", "http://www.perdu.com", "the target URL to redirect the request to") |
||||
|
} |
||||
|
|
||||
|
// The MyResponseWriter is a wrapper around the standard http.ResponseWriter
|
||||
|
// We need it to retrieve the http status code of an http response
|
||||
|
type MyResponseWriter struct { |
||||
|
Underlying http.ResponseWriter |
||||
|
Status int |
||||
|
} |
||||
|
|
||||
|
func (mrw *MyResponseWriter) Header() http.Header { |
||||
|
return mrw.Underlying.Header() |
||||
|
} |
||||
|
|
||||
|
func (mrw *MyResponseWriter) Write(b []byte) (int, error) { |
||||
|
return mrw.Underlying.Write(b) |
||||
|
} |
||||
|
|
||||
|
func (mrw *MyResponseWriter) WriteHeader(s int) { |
||||
|
mrw.Status = s |
||||
|
mrw.Underlying.WriteHeader(s) |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
// Parse the command line arguments
|
||||
|
flag.Parse() |
||||
|
|
||||
|
// Parse the target URL and perform some sanity checks
|
||||
|
url, err := url.Parse(target) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
|
||||
|
// Initialize a Reverse Proxy object with a custom director
|
||||
|
proxy := httputil.NewSingleHostReverseProxy(url) |
||||
|
underlying_director := proxy.Director |
||||
|
proxy.Director = func(req *http.Request) { |
||||
|
// Let the underlying director do the mandatory job
|
||||
|
underlying_director(req) |
||||
|
|
||||
|
// Custom Handling
|
||||
|
// ---------------
|
||||
|
//
|
||||
|
// Filter out the "Host" header sent by the client
|
||||
|
// otherwise the target server won't be able to find the
|
||||
|
// matching virtual host. The correct host header will be
|
||||
|
// added automatically by the net/http package.
|
||||
|
req.Host = "" |
||||
|
} |
||||
|
|
||||
|
http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { |
||||
|
// Log the incoming request (including headers)
|
||||
|
fmt.Printf("%v %v HTTP/1.1\n", req.Method, req.URL) |
||||
|
req.Header.Write(os.Stdout) |
||||
|
fmt.Println() |
||||
|
|
||||
|
// Wrap the standard response writer with our own
|
||||
|
// implementation because we need the status code of the
|
||||
|
// response and that field is not exported by default
|
||||
|
mrw := &MyResponseWriter{ Underlying: rw } |
||||
|
|
||||
|
// Let the reverse proxy handle the request
|
||||
|
proxy.ServeHTTP(mrw, req) |
||||
|
|
||||
|
// Log the response
|
||||
|
fmt.Printf("%v %v\n", mrw.Status, http.StatusText(mrw.Status)) |
||||
|
mrw.Header().Write(os.Stdout) |
||||
|
fmt.Println() |
||||
|
}) |
||||
|
|
||||
|
fmt.Printf("Listening on port %v for incoming requests...\n", local_port) |
||||
|
http.ListenAndServe(fmt.Sprintf(":%v", local_port), nil) |
||||
|
} |
||||
|
|
||||