TOC |
|
This Internet-Draft is submitted to IETF in full conformance with the provisions of BCP 78 and BCP 79.
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF), its areas, and its working groups. Note that other groups may also distribute working documents as Internet-Drafts.
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as “work in progress.”
The list of current Internet-Drafts can be accessed at http://www.ietf.org/ietf/1id-abstracts.txt.
The list of Internet-Draft Shadow Directories can be accessed at http://www.ietf.org/shadow.html.
This Internet-Draft will expire on December 17, 2009.
Copyright (c) 2009 IETF Trust and the persons identified as the document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents in effect on the date of publication of this document (http://trustee.ietf.org/license-info). Please review these documents carefully, as they describe your rights and restrictions with respect to this document.
This document describes a method for compressing HTTP messages into a binary format to be transmitted using UDP over 6LoWPAN wireless networks.
1.
Introduction
1.1.
Requirements notation
1.2.
Security Considerations
1.3.
Terminology
2.
Datagram Format
2.1.
Format Notation
2.2.
Request Format
2.3.
Response Format
2.4.
Compressed Headers
2.5.
Mime Type Codes
2.6.
Example
3.
UDP Transmission
4.
Transaction-Id
5.
Caching
5.1.
Cache Control
5.2.
ETag Validation
5.3.
Interception Proxy Caching
5.4.
Sleeping Nodes
5.5.
Cache Refresh
5.6.
Caching non-GET Methods
6.
HTTP to Chopan Gateways
7.
Security
8.
Normative References
§
Author's Address
TOC |
The Pervasive Internet is a vision that everyday devices with microprocessors are woven into the fabric of the Internet. One of the critical emerging technologies in this domain is 6LoWPAN which enables low cost, low power devices to communicate using the Internet Protocol. 6LoWPAN is the first step towards building the Pervasive Internet. Chopan defines the next step: integrating 6LoWPAN devices with the World Wide Web to leverage the massive investment in existing URI and HTTP infrastructure.
Chopan is derived from HTTP with these changes:
TOC |
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119] (Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels,” March 1997.).
TOC |
Discussed in Section 7 (Security).
TOC |
6LoWPAN: IPv6 for Low power Personal Area Networks described in [RFC4944] (Montenegro, G., “Transmission of IPv6 Packets over IEEE 802.15.4 Networks,” September 2007.).
Compression: translation from of a TCP/HTTP text based message into a compressed binary UDP/Chopan message (gateway functionality).
Decompression: translation from of a binary UDP/Chopan message into a TCP/HTTP text based message (gateway functionality).
Gateway: a node which transparently translates between HTTP and Chopan messages.
HTTP: Hyper Text Transfer Protocol described in [RFC2616] (Fielding, R., “Hypertext Transfer Protocol -- HTTP/1.1,” June 1999.).
PAN: Personal Area Network - an IP sub-network with constrained bandwidth and/or constrained computing devices. This specification is designed for low power PANs running 6LoWPAN, but Chopan is an ideal solution for any network with bandwidth or computing restraints.
Interception Proxy Cache: a node which transparently intercepts HTTP requests to an origin server and returns cached responses on its behalf.
Origin Server: the server on which the master version of resource resides.
Resource: an abstract unit of information identified with a URI and transported over a network using a MIME typed representation.
Sleeping Nodes: battery powered network nodes which spend most of their time in a hibernation state to converse power.
TCP: Transmission Control Protocol described in [RFC0793] (Postel, J., “Transmission Control Protocol,” September 1981.).
UDP: User Datagram Protocol described in [RFC0768] (Postel, J., “User Datagram Protocol,” August 1980.).
UTF-8: Encoding of Unicode characters compatible with ASCII described in [RFC2279] (Yergeau, F., “UTF-8, a transformation format of ISO 10646,” January 1998.)
TOC |
Chopan uses a customized binary encoding for HTTP requests and responses to achieve message compression into a UDP packet. A two byte magic number is used to identify the packet as a Chopan message – “h6” for requests and “H6” for responses. Both requests and responses allow for zero or more compressed headers.
Any bytes after the headers in the packet are considered the message-body. The length of the message-body is implied by the packet length (the Content-Length header MAY be omitted). The entire message MUST fit with in a single UDP packet. When running over 6LoWPAN, messages SHOULD fit into a single 802.15.4 frame to avoid fragmentation.
TOC |
Message formats are described as a data structure using the following primitive types:
TOC |
A normal HTTP request is composed of a request-line, a set of request-headers, and the message-body. This information is compressed in the following binary format:
request { u2 magic 0x6836 – ASCII "h6" u1 method-code str uri header[] headers u1 zero byte end of headers u1[] message-body }
The HTTP request-line contains three pieces of information: the method, URI, and version. The URI is encoded as a null-terminated UTF-8 string. Standard request methods are encoded into a byte as follows:
Method Code ASCII Char ------------ ---- ---------- DELETE 0x44 D GET 0x47 G HEAD 0x48 H OPTIONS 0x4F O POST 0x50 P PUT (Update) 0x55 U TRACE 0x54 T
TOC |
A normal HTTP response is composed of a status-line, response-headers, and the message-body. This information is compressed in the following binary format:
response { u2 magic – ASCII "H6" u1 status-code header[] headers u1 zero byte end of headers u1[] message-body }
The HTTP status code is compressed into a single byte where the top 3-bits represent the 100s decimal digit, and the bottom 5-bits represent the last two decimal digits. Example of binary mappings:
1xx -> 0x2X, b001x_xxxx 2xx -> 0x4X, b010x_xxxx 3xx -> 0x6X, b011x_xxxx 4xx -> 0x8X, b100x_xxxx 5xx -> 0xAX, b101x_xxxx 200 -> 0x40 // OK 404 -> 0x84 // Not Found 415 -> 0x4F // Unsupported Media Type 416 -> 0x50 // Requested range not satisfiable 417 -> 0x51 // Expectation Failed
TOC |
Standardized HTTP request and response headers are compressed using predefined binary codes. Compressed headers are encoded as follows:
header { u1 header-code (high bit determines encoding of value) u2|str value (u2 or str based on header-code high bit) }
Headers are encoded using a 8-bit header-code which represents the header name. If the high bit (0x80) is clear in the header-code, then the value is encoded as an unsigned 16-bit integer. If the high bit is set, then the value is encoded as a null terminated UTF-8 string. The u2 value encoding allows compression on a header-by-header basis. Refer to the table below how each header utilizes a u2 value.
If an HTTP header name does not have a standard binary encoding, then it MAY be stripped at the proxy gateway, otherwise it can be passed using its string name. Uncompressed header names are encoded as follows:
uncompressed-header { u1 header-code is 0x7f (u2 val) or 0xff (str val) str name encoded as null terminated string u2|str value }
The following table defines the header codes for standard HTTP headers. Each code has the high bit clear indicating a u2 value. Mask the code with 0x80 to obtain the str value code:
Header Code Notes ------------------ ---- ------------------------------------------- End-Of-Headers 0x00 zero indicates no more headers Uncompressed 0x7F name string, u2/string value Accept 0x01 u2 val: mime type code Accept-Charset 0x02 Accept-Encoding 0x03 Accept-Language 0x04 Accept-Ranges 0x05 Age 0x06 u2 val: delta age in seconds Allow 0x07 Authorization 0x08 Awake-Time 0x09 u2 val: seconds, used with check-in request Cache-Control 0x0A u2 val: max-age in seconds Connection 0x0B unsupported Content-Encoding 0x0C Content-Language 0x0D Content-Length 0x0E u2 val: bytes; omit to imply by packet size Content-Location 0x0F Content-MD5 0x10 Content-Type 0x11 u2 val: mime type code Cookie 0x12 Date 0x13 ETag 0x14 u2 val: etag is 4 digit upper case hex str Expect 0x15 u2 val: uncompressed code 100 is 0x64 Expires 0x16 should be avoided (use max-age) From 0x17 Host 0x18 If-Match 0x19 u2 val: etag is 4 digit upper case hex str If-Modified-Since 0x1A should be avoided (use max-age) If-None-Match 0x1B u2 val: etag is 4 digit upper case hex str If-Range 0x1C If-Unmodified-Since 0x1D should be avoided (use max-age) Last-Modified 0x1E should be avoided (use age, max-age) Location 0x1F Max-Forwards 0x20 u2 val: number of hops Pragma 0x21 obsolete Proxy-Authenticate 0x22 Proxy-Authorization 0x23 Range 0x24 Referer 0x25 Retry-After 0x26 u2 val: seconds, used with 202 responses Server 0x27 Set-Cookie 0x28 Sleep-Time 0X29 u2 val: seconds, used for check-in requests TE 0x2A Transaction-Id 0x2B u2 val: same as 4 digit upper case hex str Trailer 0x2C unsupported Transfer-Encoding 0x2D Upgrade 0x2E User-Agent 0x2F Vary 0x30 Via 0x31 Warning 0x32 u2 val: uncompressed code 111 is 0x6F WWW-Authenticate 0x33
TOC |
The Accept and Content-Type headers may be compressed into an unsigned 16-bit type code using the following table:
Mime Type Code Notes ------------------------ ------ ------------------------------- application/octet-stream 0xA001 used for arbitrary binary files text/plain 0xB001 charset implied to be UTF-8 text/html 0xB002 charset implied to be UTF-8 text/xml 0xB003 charset implied to be UTF-8 text/csv 0xB004 charset implied to be UTF-8
NOTE: we also need to give thought to what kind of information models we use and how they are represented with existing or new MIME types. For example we might want to use ASN.1 MIBs, binary oBIX, etc...
TOC |
Assume the following HTTP request:
GET /pt07 HTTP/1.1 Host: sensor2086.acme.com Accept: text/plain If-None-Match: "3A7F" Cache-Control: max-age=900
The HTTP request above would be compressed into the following sequence of hexadecimal bytes:
68 36 47 2F 70 74 30 37 00 01 B0 01 1B 3A 7F 0A 03 84 00 ^ ^ ^ ^ ^ ^ ^ | | | | | | +- End | | +- URI | | +- Cache-Control | +- GET | +- If-None-Match +- magic +- Accept
Note that we stripped the Host header and compressed Accept, If-None-Match, and Cache-Control into two byte header values.
TOC |
One of the primary characteristics of Chopan is the ability to transmit HTTP requests and responses over UDP. Since HTTP was originally designed to be run over TCP, we must make some design trade-offs to layer the protocol over an unreliable packet based transport.
Chopin follows the standard HTTP request/response model. A client makes a Chopan request to a server with a request message. When the server receives the request, it sends the client back a response message.
Both the request and response messages MUST fit within one UDP packet, as such large message bodies are not supported. However, the Range header may be used to chunk the transfer of resources which do not fit a single UDP packet. When running over 6LoWPAN, messages SHOULD fit into a single 802.15.4 frame to avoid fragmentation.
Because UDP is unreliable, there is no guarantee that a server receives a request, nor that a client receives the response. If a client does not receive a response to its request after a reasonable amount of time, then it SHOULD retry the request up to three times before timing out. It is therefore possible that the server might receive the same request multiple times. A request is "retry safe" if it can be retried multiple times by the client without compromising server state. Idempotent methods like GET and HEAD MUST be retry safe. Methods such as PUT and DELETE should also be retry safe since they atomically modify or delete the resource. Methods like POST are typically not retry safe unless coupled with another mechanism. In the next section we examine an extension to HTTP for making requests retry safe with the Transaction-Id header.
UDP does not guarantee message order. Therefore, it is the client’s responsibility to impose message ordering if required. Message ordering can be maintained by waiting for a response, before sending the next request. When message ordering is not required, the client MAY have multiple simultaneous outstanding requests. This can increase throughput on networks with high latency. If performing concurrent requests, clients SHOULD use the Transaction-Id header to match responses to requests.
TOC |
Due to the unreliable nature of UDP, requests and responses do not have guaranteed delivery or ordering. This can particularly cause problems when a non-idempotent request is received successfully by the server, but the response packet is dropped. In this case the client’s expected behavior is to retry the request which might cause the server to receive the same request multiple times. For methods such as POST which are not implicitly retry-safe, we define a new header called Transaction-Id.
Transaction-Id is a unique identifier generated by the client. The tuple of the client’s IP address, port number, and Transaction-Id should be globally unique within the transaction’s temporal window. Any retries initiated by the client MUST include the same transaction id in the retry requests.
When a server receives a request with a Transaction-Id header, it MUST pass the identifier back to the client via the response’s Transaction-Id header. The server MAY also choose to utilize the Transaction-Id to implement "at-most-once" semantics. It is a server local matter to decide how to apply the transaction id for a given HTTP method and resource.
If a client attempts to request a method on the resource which requires a Transaction-Id header and fails to specify one, then the server SHOULD respond with 400 Bad Request.
TOC |
6LoWPAN networks are typified by a gateway device which acts as a router or bridge between the PAN and the external IP network. Often the external IP network is physically connected by high a bandwidth technology such as Ethernet or WiFi. The PAN itself typically has low bandwidth and is composed of resource constrained nodes. Often times the nodes in a PAN are battery powered, and spend most of their time sleeping.
Because of this physical architecture, it is desirable for the more capable nodes in the PAN to serve as caches for the more constrained devices. Effective use of caching enables Chopan to optimize both bandwidth on the PAN and power on constrained devices. In the case of a sleeping node, it allows proxies to immediately return cached representations of resources.
TOC |
HTTP (Fielding, R., “Hypertext Transfer Protocol -- HTTP/1.1,” June 1999.) [RFC2616] defines a sophisticated caching model in sections 13 and 14.9. This model has multiple caching features, often with overlapping functionality. Since Chopan is targeted for resource constrained devices, this specification recommends use of a subset of the HTTP caching model based on resource age and max-age.
It is expected that most resources accessed by Chopan are representations of sensor data. The nature of the sensor data determines its cache life. For example a temperature sensor in a room is likely to change very slowly, so it might have a cache life of fifteen minutes. But a temperature sensor in an oven might have a cache life of only ten seconds before it is considered stale data. Chopan uses existing HTTP caching features to give both the client and server a say in cache management.
When an origin server publishes a resource representation via a GET request, it SHOULD specify the Age header. For example if a resource represents a sensor, and that sensor was read 4 seconds ago, then the Age header should be set to 4 seconds. If the resource has an age less than 1 second, then set the Age header to 0. The Age header SHOULD be compressed into a two byte value if less than 18.2 hours.
In cases when the origin server has knowledge about the cache life of a given resource, it SHOULD set the Cache-Control header with a Max-Age directive. Note that the two byte value encoding of Cache-Control is implied to be Max-Age as a number of seconds. When the server specifies Max-Age, it is directing upstream proxies and clients how long to cache the resource. For example if a resource specifies an Age of 4 seconds, and a Max-Age of 30 seconds, then the resource should be cached for 26 seconds before it is considered stale.
Clients MAY also specify the Cache-Control header with a Max-Age directive on requests. In this case, the client is directing the maximum amount of staleness which may be tolerated. For example if a client requests a resource with a Max-Age of 10 seconds, and the resource has an age of 8 seconds, then the server may respond with the cached resource. If however the resource has an age older then 10 seconds, then the server should refresh its cache. In the case of a proxy cache, this means contacting the origin server. In the case of the origin server, it may require polling the sensor.
A resource is considered stale if its Age is greater than either Max-Age specified by the server or the Max-Age specified by the client. If a server node has a cached version of a resource which is stale, it SHOULD always attempt to refresh its cache. If the cache cannot be refreshed immediately because of normal operation (for example the origin server is a sleeping node), then the stale resource should be returned and the Warning header SHOULD be specified with the 110 status code (response is stale). If cache refresh fails abnormally (for example the origin server cannot be contacted), then the stale resource SHOULD be returned and the Warning header specified with the 111 status code (revalidation failed).
TOC |
Key to any caching strategy is cache validation, the mechanism used by a client or proxy to refresh its cache with the origin server. Often even though a cached resource has expired, the original resource hasn’t been modified. But in order to avoid re-transmitting the entire resource the client and server must define a mechanism to validate the cached copy. In HTTP this validation may be negotiated using either timestamps or entity tags. Chopen discourages the use of timestamps because often nodes do not support time clocks. Instead entity tags SHOULD be used for cache validation.
An entity tag is an opaque hash of a given resource’s version. It is defined by the origin server using the ETag header. If possible, a two byte etag should be used to allow for optimal compression. If an etag was specified for a cached resource, then clients and proxies SHOULD specify the If-None-Match header on cache refresh. The server SHOULD respond with a 304 (Not Modified) response if the etag has been not modified.
TOC |
In Chopan, caching is done transparently to the client via "interception caching". Interception caching is a commonly used technique used to insert HTTP caches between clients and origin servers, without requiring client configuration. Clients send packets to the origin server directly, but as these packets are routed into the PAN, one of the routing nodes processes the message directly on behalf of the origin server. This architecture requires that routing nodes in the PAN are actively examining the packets before they are routed to their destination address.
The downside to using interception caching, is that technically it breaks the encapsulation of the IP stack - routing nodes must become aware of an application level protocol. The upside to this design, is that client nodes do not have be explicitly configured to know about the proxies for every PAN. Since PANs have the potential to add billions of new nodes to the Internet, it seems reasonable to trade-off the purity of IP routing within the PAN to maintain the simplicity of the Internet at large.
Interception caches SHOULD use a combination of the destination port and the packet’s magic two byte marker to sniff Chopan packets. By default we assume Chopan runs on UDP port 80, although proxies SHOULD make this configurable.
The lifecycle of an interception cache request:
This lifecycle assumes that the origin server is a powered device which is awake during normal operation. If the origin server is a battery powered device then the origin server is mostly likely sleeping. This use case is discussed further in Sleeping Nodes section.
Interception proxies SHOULD be transparent to the client. However, when a proxy communicates directly with the origin server it has a choice to forward the client’s original packet (with the client’s IP address), or to initiate a new request (with the proxy’s IP address). Proxy’s SHOULD initiate new requests using the proxy’s own IP address. This means that origin servers are effectively responding directly to the proxy with no knowledge of the original client request. The disadvantage of this model is that it breaks end-to-end communication principles of the Internet. However this model provides significant advantages:
TOC |
PANs commonly include battery powered nodes which spend most of their time sleeping to conserve power. These nodes periodically wake up to check sensors, perform computation, and catch up on network communications. Because of their nature, sleeping nodes do not make for reliable origin servers. Chopan handles this use case by fronting all sleeping nodes with interception caches. This allows all requests for resources on the sleeping nodes to be transparently brokered by proxies. Proxies then synchronize their caches with the sleeping nodes periodically during a "check-in" process.
The lifecycle for interception caching of sleeping nodes follows the standard interception model detailed above. However, when a request is made for a resource the proxy doesn’t have in its cache, the request cannot be immediately fulfilled. In this case the proxy SHOULD return a 202 Accepted response indicating that background processing is required before the request can be completed (waiting for the sleeping node to wake up). The Retry-After header SHOULD be set indicating the number of seconds before the request should be tried again. The retry time should be based on the time it will take the sleeping node to wake up, check-in, and give the proxy a chance to refresh its cache. The Retry-After header can be estimated from the Awake-Time and Sleep-Time headers (see below).
Sleeping nodes MUST be configured to check-in with their proxy or proxies when they wake up. This is done by sending a POST request to the "/ci" URI of each proxy. When a proxy node receives a check-in request, it SHOULD respond with 200 OK response. The sleeping node SHOULD use standard retry/timeout mechanism to ensure that the check-in is received by the proxy. After the sleeping node has checked-in, then the proxy SHOULD poll for all the resources in its cache which require refreshing. This will include all new pending resources which resulted in 202 responses. After the sleeping node has given the proxy a chance to refresh its cache, it can go back to sleep.
Sleeping nodes SHOULD specify the Awake-Time and Sleep-Time headers in their check-in request. The Awake-Time header specifies how long the node expects to stay awake to give the proxy a chance for cache refresh. The Sleep-Time indicates how long the node expects to sleep before the next check-in. A proxy should expect the next check-in after the sum of Awake-Time and Sleep-Time has elapsed - this period can then be used for estimating the proxy’s Retry-After header.
TOC |
Chopan proxies can take an active or a passive approach to cache refresh. In a passive model, stale resources are allowed to expire and are eventually flushed from the cache. New requests for the resources are forwarded to the origin server, and the response is used to refresh the cache. On the other hand, the proxy can actively poll origin servers to refresh cached resources independent of client requests.
For sleeping nodes, proxies MUST actively refresh their cache. This is required because there are only limited windows of opportunity while the node is awake for the proxy to refresh resources.
When the origin server is a powered node, either active or passive cache refresh may be used. Using active refresh to proactively keep caches refreshed can potentially decrease the latency of external requests.
Cached resources can be in one of the following states:
TOC |
In most circumstances, clients make GET requests to retrieve representations of resources. In this case, proxies are caching the response which contains that resource representation. However clients may also perform POST, PUT, or DELETE requests. In the case where the origin server is a powered node, these requests SHOULD always be immediately forwarded to the origin server.
However in the case of sleeping nodes, the proxy MUST cache the request itself until the node wakes up and checks-in. Without this functionality it would be impossible to perform these HTTP methods on sleeping nodes. Non-GET methods to sleeping nodes MUST use a Transaction-Id to associate the request with a specified client IP address, port number, and transaction id.
Let’s consider a transaction for a resource POST on a sleeping node:
TOC |
Chopan leverages the HTTP standard in order to provide interoperability with the World Wide Web. Interoperability is achieved by using standard HTTP external to the PAN and using Chopan internal to the PAN. Nodes which perform HTTP-Chopan translation are called Chopan gateways:
Diagram of gateway translations:
<= External | PAN => Client -> [HTTP] -> Gateway -> [Chopan] -> Server Server <- [HTTP] <- Gateway <- [Chopan] <- Client
Gateway translations SHOULD be performed transparently. Clients external the PAN assume they are communicating HTTP directly to the origin server. Gateways intercept these HTTP requests and translate them into Chopan requests. Likewise responses are translated from Chopan back to HTTP.
Because Chopan recommends that translation happens transparently, this means that the gateway must be sniffing incoming packets for TCP/HTTP requests. This design has all the same issues as detailed in Interception Proxy Caching. It is expected that in most implementations the gateway will also perform interception caching, although this specification does not require it.
HTTP to Chopan is referred to as compression, and Chopan to HTTP is referred to as decompression. During the compression process the text format of requests and responses is encoded into Chopan’s binary message format. Each HTTP header is examined and mapped into its binary encoding. Depending on the quality of the PAN link layer, the compression process may strip out HTTP headers, according to these priorities:
The Chopan compression and stripping of headers is a gateway to origin server matter. This does not free the gateway from faithfully implementing the full HTTP specification and abiding by its conventions. In the cases where HTTP headers or functionality is reduced to meet Chopan constraints, the gateway should compensate so that the client’s perspective is communication with a fully compliant HTTP origin server.
TOC |
Ideally Internet protocols implement an end-to-end security model between the two endpoint nodes. However it is difficult to implement end-to-end session based security with unreliable packet protocols and sleeping nodes. Rather Chopan, recommends that the security strategy is divided between internal and external PAN nodes.
Internally all PAN nodes should be fully trusted using link layer security such as the AES encryption specified by 802.15.4.
External to the PAN, the gateway should utilize full TCP/HTTP to enable the well known security mechanisms associated with those protocols. This includes TLS/HTTPS and the various HTTP authentication mechanisms.
NOTE: A lot more to think about here...
TOC |
[RFC0768] | Postel, J., “User Datagram Protocol,” RFC 768, August 1980. |
[RFC0793] | Postel, J., “Transmission Control Protocol,” RFC 793, September 1981. |
[RFC2119] | Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels,” BCP 14, RFC 2119, March 1997 (TXT, HTML, XML). |
[RFC2279] | Yergeau, F., “UTF-8, a transformation format of ISO 10646,” RFC 2279, January 1998. |
[RFC2616] | Fielding, R., “Hypertext Transfer Protocol -- HTTP/1.1,” RFC 2616, June 1999. |
[RFC4944] | Montenegro, G., “Transmission of IPv6 Packets over IEEE 802.15.4 Networks,” RFC 4944, September 2007. |
TOC |
Brian Frank | |
Tridium, Inc | |
Richmond, VA | |
US | |
Email: | brian.tridium@gmail.com |