Using Pangea API in a HTTP Proxy with Wick
This blog will walk through the process of building a HTTP proxy, using Wick, that will use the Pangea Cloud API to enrich the request with the user’s location and then forward the request to the appropriate server.
This will allow any existing service to be location aware without having to change the source code of service itself. This proxy can be run as a sidecar or in kubernetes or as a standalone service or ingress for adding the location data for multiple services.
Finally, we will install the same component as a CLI tool to show how the same component can be reused without ever having to be recompiled.
What are we building?
What is Wick?
Wick is an opinionated framework for building applications. It brings Hexagonal Architecture and WebAssembly together to make it easy to build and deploy maintainable applications. If you are not familar with Hexagonal Architecure, this is a really informative video.
With Wick, you are in full control of the resources that you application can access and all code is executed in a sandbox. With these security guarantees, we are moving the world to a future where sourcecode does not matter. This will be especially important as AI generated code becomes more prevelant. We need the ability to run code without having to trust the source. That is what Wick is fundamentally about.
You can learn more about Wick at https://candle.dev/docs and view the source code and examples on Github. In short, Wick is like an orchestrator and service mesh for the functions inside an application.
What is the point of this blog series?
There are multiple points that will be showcased here:
- It is possible to build practical application using WebAssembly on the server side. If you are instersted in more disucssions around
Practical WebAssembly
, check out our podcast! - The same component can be reused without ever having to be being recompiled. This can be run as a CLI tool or a service or a sidecar.
- Wick is a great framework for building applications.
- The Pangea Cloud API is a great set of API’s for adding security into your applications.
Getting started
If you want to follow along, you will need to install Wick. You can do that by running the following command:
- OSX / Linux:
curl -sSL sh.wick.run | bash
- Windows (Powershell):
curl https://ps.wick.run -UseBasicParsing | Invoke-Expression
The complete source code for this project is available on Github.
Building the components
Pangea Cloud offers a rest-based API. We will first create a simple consumer of their IP Geolocation API.
In terminal, we run the following command:
wick new component http pangea_api.wick
This generates a boilerplate HTTP component that we can then modify to call the Pangea API.
Here is what the final component looks like:
kind: wick/component@v1
name: pangea_api
metadata:
version: 0.1.0
description: HTTP Client Component for Pangea Cloud API
licenses:
- Apache-2.0
resources:
- name: HTTP_URL
resource:
kind: wick/resource/url@v1
url: '{{ ctx.root_config.url }}'
component:
kind: wick/component/http@v1
with:
- name: token
type: string
- name: url
type: string
resource: HTTP_URL
codec: Json
operations:
- name: ip_geolocate
inputs:
- name: ip
type: string
method: Post
codec: Json
path: v1/geolocate
headers:
"Authorization": ["Bearer {{ ctx.root_config.token }}"]
"Content-Type": ["application/json"]
body:
ip: "{{ ip }}"
The HTTP component has a few different sections: resources
, component
, and operations
.
The resources
section defines the resources that the component will use. In this case, we are defining a single resource, HTTP_URL
, that is of type wick/resource/url@v1
. This resource is used in the component
section. Wick currently supports url
and dir
resources for exposing remote services and directories to the component. The component has no access to the outside world other than what is defined in the resources section.
The component
section defines the component itself. The with
section defines the inputs that the component will accept. In this case, we are defining two inputs, token
and url
, that are of type string
. The resource
section defines the resource that the component will use. In this case, we are using the HTTP_URL
resource that we defined above. The codec
section defines the codec that will be used to encode and decode the data. In this case, we are using Json
.
The operations
section defines the operations that the component will expose. In this case, we are defining a single operation, ip_geolocate
, that takes a single input, ip
, that is of type string
. The method
section defines the HTTP method that will be used. In this case, we are using Post
. The codec
section defines the codec that will be used to encode and decode the data. In this case, we are using Json
. The path
section defines the path that will be appended to the url
resource. The headers
section defines the headers that will be sent with the request. The body
section defines the body that will be sent with the request.
You will see that in the headers
block, we have exposed some variables using Liquid
syntax. This allows us some flexibility on getting customized data into the component and allows for the component yaml to use variables.
Checking the component
Sign up for a free account at https://pangea.cloud and get an API token. Save the token and url as environment variables PANGEA_TOKEN
and PANGEA_URL
You can then run the following command to test the component:
wick invoke pangea_api.wick ip_geolocate --with="{\"token\": \"$PANGEA_TOKEN\", \"url\": \"$PANGEA_URL\"}" -- --ip="93.231.182.110"
{"payload":{"value":{"headers":{"access-control-allow-headers":["*"],"access-control-allow-methods":["*"],"access-control-allow-origin":["*"],"access-control-max-age":["86400"],"content-length":["382"],"content-type":["application/json"],"date":["Fri, 04 Aug 2023 15:39:52 GMT"],"server":["Pangea API Server"],"set-cookie":["AWSALB=yt1mpqgn5i1f4lRVAju3QY/naZMKtTJeWv60N1rEZuhL5KRExDDycyts0+gxCuPG5iNMmemmKQV8mxJ09uZAJ2cT22GFKRdN3W1uEtDu3D5c2IMCtuMrki+7OMta; Expires=Fri, 11 Aug 2023 15:39:52 GMT; Path=/","AWSALBCORS=yt1mpqgn5i1f4lRVAju3QY/naZMKtTJeWv60N1rEZuhL5KRExDDycyts0+gxCuPG5iNMmemmKQV8mxJ09uZAJ2cT22GFKRdN3W1uEtDu3D5c2IMCtuMrki+7OMta; Expires=Fri, 11 Aug 2023 15:39:52 GMT; Path=/; SameSite=None; Secure"],"x-pangea-server-id":["05c5ccf3-9fbc-4c63-8e69-8c8e4fcd5a76"],"x-ratelimit-limit":["1500"],"x-ratelimit-remaining":["1499"],"x-ratelimit-reset":["0"],"x-request-id":["prq_onblhssfsnr42arh6hby2nu6rnhqucw6"]},"status":"200","version":"2.0"}},"port":"response"}{"payload":{"value":{"request_id":"prq_onblhssfsnr42arh6hby2nu6rnhqucw6","request_time":"2023-08-04T15:39:52.339697Z","response_time":"2023-08-04T15:39:52.367411Z","result":{"data":{"city":"unna","country":"Federal Republic Of Germany","country_code":"de","latitude":51.56,"longitude":7.65,"postal_code":"59425"}},"status":"Success","summary":"IP location found (Country: Federal Republic Of Germany)"}},"port":"body"}
Simplifying the response
As you can see, the payload response is pretty large and for our usecase, we want to just get the country_code
portion and don’t need the rest of the body or any of the response headers.
{
"payload": {
"value": {
"request_id": "prq_ujgqrcrxm2s54sw3x43z7nisez5rhhzj",
"request_time": "2023-08-07T19:04:47.274577Z",
"response_time": "2023-08-07T19:04:47.299400Z",
"result": {
"data": {
"city": "unna",
"country": "Federal Republic Of Germany",
"country_code": "de",
"latitude": 51.56,
"longitude": 7.65,
"postal_code": "59425"
}
},
"status": "Success",
"summary": "IP location found (Country: Federal Republic Of Germany)"
}
},
"port": "body"
}
For this, we will create a composite
component. A composite component is a component that is made up of other components. In this case, we will create a component that uses the pangea_api
component that we created above and then simplifies the response.
kind: wick/component@v1
name: pangea
metadata:
version: 0.1.0
description: pangea component
licenses:
- Apache-2.0
package:
registry:
host: registry.candle.dev
namespace: pangea
import:
- name: pangea_api
component:
kind: wick/component/manifest@v1
ref: ./pangea_api.wick
with:
token: '{{ ctx.root_config.token }}'
url: '{{ ctx.root_config.url }}'
component:
kind: wick/component/composite@v1
with:
- name: token
type: string
- name: url
type: string
operations:
- name: ip_country_code
flow:
- <input>.ip -> pangea_api::ip_geolocate[GEO].ip
- GEO.body.result.data.country_code -> <output>.country_code
- GEO.response -> drop
- name: ip_geolocate
flow:
- <input>.ip -> pangea_api::ip_geolocate[GEO].ip
- GEO.body.result.data -> <output>.geolocation
- GEO.response -> drop
If you look at the operations
, you will see that we created two operations. ip_country_code
calls the ip_geolocate component and then use the ->
operator to pipe the response from the component to the next operation. We use a simple dot based notation to parse the body object that is returned and return just a subset of the response. We then use the drop
operation to drop the unused response components from the previous operation.
“What if I wanted to do something more complex?”
If you had this question, I wanted to take some time to answer it directly. If you must write code becuase your application is complex then you are absolutely welcome to create a component that is created using the Rust programming language and then compiled to WebAssembly. You can then use that component as part of your flow to handle complex conditional logic, data transformations, etc.
The beauty of Wick is that you never need to share your source code. The yaml manifest provides the interface to your component and Wick provides all of the security. It is impossible for a component to do more than what is defined in the manifest. You can read my blog post on LinkedIn to understand why I believe that source code does matter.
I made a WASM component so you can see what it takes to write your own component. You can read more about how to make a WASM component in our documentation.
But again, I am making a CLI app and a proxy middleware without writing or compiling any code. I am just using Wick to compose configurable components together. So let’s get back to that.
Checking the composite component
You can run the component directly by using the wick invoke
command to run the component operation directly.
wick invoke pangea.wick ip_country_code --with="{\"token\": \"$PANGEA_TOKEN\", \"url\": \"$PANGEA_URL\"}" -- -- ip="99.101.46.99"
{"payload":{"value":"us"},"port":"country_code"}
Making a middlware component
In Wick, we have the concept of interface types
. These are like structs that can be shared between applications to allow for simpler interfacing. We will use the http
type to import the interface definition for a HttpRequest
and RequestMiddlewareResponse
. We will also import multiple other components and tie them together to make a complete flow for our middleware component.
RequestMiddlewareResponse
is a type is a union return type of either HTTP request or a HTTP response. With Wick, a request handled by a middleware can return either a HTTP request or HTTP response.
Connecting multiple components
To illustrate the process, let’s break down the flow of our middleware into the following steps:
1. Get the client IP from the HttpRequest
using the http_client_ip
component
2. Pass the IP address to the pangea
component to get the country code
3. Append a request header to the HttpRequest
with the country code using the http_headers
component
4. Return the modified HttpRequest
as a RequestMiddlewareResponse
.
Using our flow syntax, we pass the data between the components. The ->
operator is used to pass the output data from one component to the input data for the next. The drop
operator is used to drop the data from the previous component so Wick does not expect for a component input to connect to that output. The RequestMiddlewareResponse
is returned as the output of the middleware component.
This is the result of the following YAML manifest:
kind: wick/component@v1
name: middleware
metadata:
version: 0.0.1
description: middleware component for pangea
licenses:
- Apache-2.0
package:
registry:
host: registry.candle.dev
namespace: pangea
import:
- name: http
component:
kind: wick/component/types@v1
ref: registry.candle.dev/types/http:0.4.0
- name: pangea
component:
kind: wick/component/manifest@v1
ref: registry.candle.dev/pangea/pangea:0.1.0
with:
token: '{{ ctx.root_config.token }}'
url: '{{ ctx.root_config.url }}'
- name: client_ip
component:
kind: wick/component/manifest@v1
ref: registry.candle.dev/candle/http_client_ip:0.2.0
- name: http_headers
component:
kind: wick/component/manifest@v1
ref: registry.candle.dev/candle/http-headers:0.1.0
component:
kind: wick/component/composite@v1
with:
- name: token
type: string
- name: url
type: string
operations:
- name: enrich_request
inputs:
- name: request
type: http::request
outputs:
- name: output
type: http::RequestMiddlewareResponse
uses:
- name: ADD_HEADER
operation: http_headers::add
with:
header: x-pangea-country-code
flow:
- <>.request -> client_ip::get_ip -> pangea::ip_country_code -> ADD_HEADER.value
- <>.request -> ADD_HEADER.input
- ADD_HEADER.output -> <>.output
tests:
- name: update_request
with:
token: '{{ ctx.env.PANGEA_TOKEN }}'
url: '{{ ctx.env.PANGEA_URL }}'
cases:
- name: get_country_code
operation: enrich_request
inputs:
- name: request
value:
method: Get
scheme: Http
path: '/'
uri: 'http://localhost:8080/'
version: Http11
authority: 'localhost:8080'
remote_addr: '91.108.2.98'
query_parameters: {}
headers:
host:
- 'localhost:8080'
user-agent:
- 'curl/7.64.1'
accept:
- '*/*'
outputs:
- name: output
value:
authority: 'localhost:8080'
headers:
accept:
- '*/*'
host:
- 'localhost:8080'
user-agent:
- 'curl/7.64.1'
x-pangea-country-code:
- 'ru'
method: Get
path: '/'
remote_addr: '91.108.2.98'
scheme: 'Http'
uri: 'http://localhost:8080/'
version: '1.1'
- name: output
flag: Done
Testing the middleware component
The other new concept introduced here is the idea of a test. We have been testing the components with wick invoke
, but now we have put in an integration test for our middleware component.
wick test middleware.wick
Using the middleware component
We created a simple HTTP proxy application using Wick. We created a docker-compose file with two services:
1. HTTPbin
2. Wick Proxy with Pangea Middleware
Here is the YAML for our proxy server.
kind: wick/app@v1
name: proxy
metadata:
version: 0.0.1
description: HTTP Proxy for middleware
licenses:
- Apache-2.0
resources:
- name: httpserver
resource:
kind: wick/resource/tcpport@v1
port: "8080"
address: 0.0.0.0
- name: httpbin
resource:
kind: wick/resource/url@v1
url: http://httpbin:80
import:
- name: middleware
component:
kind: wick/component/manifest@v1
ref: ./middleware.wick
with:
token: "{{ ctx.env.PANGEA_TOKEN }}"
url: "{{ ctx.env.PANGEA_URL }}"
triggers:
- resource: httpserver
kind: wick/trigger/http@v1
routers:
- kind: wick/router/proxy@v1
middleware:
request:
- middleware::enrich_request
path: /
url: httpbin
Resources
Here we have defined two resources. The first is a TCP port resource that will listen on port 8080
. The second is a URL resource that will be used to proxy requests to the HTTPbin service. Wick will only allow access to the data that is added as a resource or passed to the application as environment variables.
In order to start the application, we need to have the PANGEA_TOKEN
and PANGEA_URL
environment variables set.
Putting it all together
We can now start the application with the following command:
docker-compose up -d
We can now test the application with the following command:
curl -X POST -v 'localhost:8080/post?show_env=1' -H "x-forwarded-for: 11.108.2.98"
This will show the request headers after they have been modified by the middleware component. Notice that the ”X-Pangea-Country-Code”: “us”
has been added to the request by the middleware.
{
"args": {
"show_env": "1"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin",
"User-Agent": "curl/7.88.1",
"X-Forwarded-For": "11.108.2.98, 172.28.0.1",
"X-Pangea-Country-Code": "us"
},
"json": null,
"origin": "11.108.2.98, 172.28.0.1",
"url": "http://httpbin/post?show_env=1"
}
Conclusion
In this tutorial, we have learned how to create a middleware component that can be used to modify the request headers of an HTTP request. We have also learned how to create a Wick application that uses the middleware component to modify the request headers of an HTTP request. We have also learned how to test the middleware component and the Wick application that uses the middleware component.
This may seem like a heavy lift, but if you look at the components like the middleware component, you can quickly see how the implemetation can be changed or added upon. That is the hidden power of Wick. Everything is put together like legos. This example can be easily extended to add other functionality from Pangea, such as their data masking service to mask private data as it passes to/from your application.
If you want geoip lookup functionality as a CLI, we created a CLI app using the same Pangea component that we created here. You can use the CLI app by installing it with the following command (make sure you have your environment variables set):
wick install registry.candle.dev/pangea/geolocate:latest
If you have any questions about using anything in this blog or building your own Wick component or application, we would love to help! Join our Discord.