Protecting HLS streaming with Google Media CDN Dual Token authentication using HMAC tokens.

Vivek Sinha Anurag
Google Cloud - Community
10 min readMay 25, 2024

In today’s digital age, delivering high-quality media content efficiently and securely is crucial for any online platform. Google Media CDN plays an important role in ensuring fast, reliable, and secure delivery of media content to users worldwide. One powerful feature of Media CDN is dual token authentication, which provides enhanced security for media URLs. In this blog post, we’ll walk you through the steps to set up Media CDN dual token configuration.

What is Dual Token Authentication?

Dual token authentication adds an extra layer of security by requiring two tokens to access media content. This prevents unauthorized access and ensures that only legitimate users can stream or download the media. The two tokens typically include a URL prefix and a secure hash, along with an expiration time. The tokens are passed in the form of query string parameters, or in the form of cookies. In this example, we will make use of cookie-less tokens, where tokens will be passed in the URL query string parameters.

Setting up Media CDN configuration.

Setting up simple Media CDN configuration is easy task. But before we dive into the actual setup, lets quickly understand the difference in setup with live and VOD. Whether you want to deliver HLS VOD stream, or HLS Live stream, the setup is very much similar. Only different in setup between live and VOD is that, for VOD you can cache everything for a long period of time. But, for live streaming, you need to be little careful for caching rules. For Live HLS stream, the master manifest file is usually static, and hence can be cached for for long period of time. The child manifest file changes very frequently, after every chunk addition. Hence, the caching duration of child manifest file should not be more than the chunk duration. In fact, the standard practice is to cache the child manifest file for (duration)/2 sec. The ts chinks can be cached for short period, like 5 mins.

Summary of caching rule for VOD:

  1. Master manifest file (usually matched with /**/master.m3u8) — 365 days
  2. Child manifest file (usually matched with /**.m3u8)— 365 days
  3. ts chunks (usually matched with /**.ts)— 365 days

Summary of caching rule for Live streaming (with 4 sec chunk size):

  1. Master manifest file (usually matched with /**/master.m3u8) — 8 hours
  2. Child manifest file (usually matched with /**.m3u8) — 2 sec
  3. ts chunks (usually matched with /**.ts) — 300 sec

Media CDN setup

In case you do not know how to enable SSL, you can follow the tutorial posted by David Reisfeld

Setup 3 routing rules.

Rule 1 matching for master manifest file (/**/master.m3u8).

Rule 2 matching on child manifest file (/**.m3u8).

Rule 3 matching on ts chunks (/**.ts).

It should look something like this.

Media CDN Routing setup

In case want to delivery many streams from single setup, you may feel free to add more rules accordingly. It is also a good practice to enable logging. The field will allow you to sample the logs. The value varies between 0 to 1, where 1 will mean 100%. In case you just want to sample 10% of logs, then value should be 0.1

Media CDN SSL and logging setup

Update the service. From here on, we will switch from the UI to command line tool for setting up advanced rules in the form of YAML.

Once you have the certificate provisioned in your account, the following lines should be included in your YAML file to include the SSL certificate with your Media CDN setup

edgeSslCertificates:
- projects/my-media-project/locations/global/certificates/my-cert

Update the “my-media-project” and “my-cert” with the relevant names.

The easiest way to edit the YAML file is use Cloud Shell, download the YAML file, make the edits, and then upload the updated YAML file.

To download the YAML file of your Media CDN use the following:

gcloud edge-cache services export <Edge-Cache-Service-Name> --destination=<edge-cache-service-name>.yaml

Now you have the YAML file, make the edits as per need. I prefer edition using the UI editor provided with the Cloud Shell. Once the YAML is updated, import the YAML file using the following command

gcloud edge-cache services import <Edge-Cache-Service-Name> --source=<edge-cache-service-name>.yaml

Dual Token Setup

The flow of Dual token setup looks like the following

Token Flow at Media CDN

In Dual toke protection, there are 2 tokens used, a short token and a long token. Short token is used for protecting master manifest file. The expiry is this token is short, hence the name short token. The customers keep value under a minute. This is created from the secret generated by user, and stored in the Secret manager. The long token is managed by Google.

Step 1: Generate Short secret

There are various ways to generate short tokens. In this example, we will generate a secret, using python

python3 -c "import secrets;open('py-shared.secret','wb').write(secrets.token_bytes(32))"

This should generate a secret file like the following

admin_@cloudshell:~/test (demo)$ python3 -c "import secrets;open('py-shared.secret','wb').write(secrets.token_bytes(32))"
admin_@cloudshell:~/test (demo)$ ls -l py-shared.secret
-rw-rw-r-- 1 admin_ admin_ 32 May 25 18:50 py-shared.secret

Download this secret file to your local system.

Download file from Cloud Shell

Now go to Secret manager, and create a secret by upload the secret file you downloaded in the previous step. Lets give it a name called shorttoken. Once created, click on shorttoken, and then it should show you version 1. Click on the 2 dots under “Actions” and select “view secret value”. It should show something like the following

Copy this hex value in a text editor, get rid of spaces, and then convert this into base64 value. You may use a 3rd party website like this one. It should give you a base64 encoded value. Keep it safe, because this will be used for generating your short token. Eg

LGRWJKHKHKK76S7S7H7R8KHJHKJHJHGG1MuY=

Next step is to allow permission to Media CDN to access this secret, which it will use to validate the tokens in the incoming request. To allow permission, run the following gcloud command

gcloud secrets add-iam-policy-binding projects/PROJECT_NUMBER/secrets/SECRET_ID \
--member="serviceAccount:service-PROJECT_NUMBER@gcp-sa-mediaedgefill.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"

Eg, for my project, it will be

gcloud secrets add-iam-policy-binding projects/719187342121/secrets/shorttoken \
--member="serviceAccount:service-123456789012@gcp-sa-mediaedgefill.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"

Now, our secret manager setup for short token is ready with the right permissions. Next step is to create a keyset using this secret manager. Keysets will be referenced in the YAML file for applying dual token.

Step 2: Generate Keysets

We will creates 2 keys sets in this section. 1st would be a short token keyset (lets call it shortkeyset) and then a long token keyset (lets call it longkeyset).

Run the following gcloud command to generate shortkeyset from the secret manager entry we created (with name shorttoken).

gcloud edge-cache keysets create shortkeyset \
--validation-shared-key='secret_version=projects/PROJECT_NUMBER/secrets/SECRET_ID/versions/KEY_VERSION'

Eg, for my project it will be

gcloud edge-cache keysets create shortkeyset \
--validation-shared-key='secret_version=projects/123456789012/secrets/shorttoken/versions/1'

Now we will create a Long token keyset (longkeyset). Since this will be google managed, we can simple run the following command to create one

gcloud edge-cache keysets create prod-longkeyset \
--public-key='id=google-managed-key,managed=true'

Now we have the keysets created. Next step is to create update the YAML file with guidelines to enable token, and apply references to keysets.

The official documentation is here, which explains the steps we did using gcloud, and console.

Step 3: Update YAML file.

Download the YAML file, using the gcloud command and open in editor

Command to download/export your YAML file

gcloud edge-cache services export <Edge-Cache-Service-Name> --destination=<edge-cache-service-name>.yaml

You need to modify 3 sections

  1. master manifest file match block
- description: Master manifest file match
headerAction:
responseHeadersToAdd:
- headerName: version
headerValue: '5'
- headerName: x-cache-status
headerValue: '{cdn_cache_status}'
- headerName: x-client-city
headerValue: '{client_city}'
matchRules:
- pathTemplateMatch: /**/manifest.m3u8
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 1
routeAction:
cdnPolicy:
addSignatures:
actions:
- GENERATE_TOKEN_HLS_COOKIELESS
copiedParameters:
- PathGlobs
- URLPrefix
keyset: longkeyset
tokenQueryParameter: hdntl
tokenTtl: 86400s
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: shortkeyset
signedRequestMaximumExpirationTtl: 60s
signedRequestMode: REQUIRE_TOKENS
signedTokenOptions:
allowedSignatureAlgorithms:
- HMAC_SHA_256
- ED25519
- HMAC_SHA1
tokenQueryParameter: hdnts
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s

Here the following code block is just for adding some metadata headers which tells me what version of my config is live, what is the cache status of the response (cache hit/miss), and Viewer’s city

headerAction:
responseHeadersToAdd:
- headerName: version
headerValue: '5'
- headerName: x-cache-status
headerValue: '{cdn_cache_status}'
- headerName: x-client-city
headerValue: '{client_city}'

The query string parameter name hdntl and hdnts is configurable. You may optionally include additional parameters like IP, session, start time, end time, headers and additional query string parameters. It is well documented here.

Note that we are Generating long tokens in this block and will be appending them to child manifest file URLs (Cookie-less). Hence the directive GENERATE_TOKEN_HLS_COOKIELESS. We are adding CORS headers towards the end. Feel free to modify the values as per your need. The directive signedRequestMode: REQUIRE_TOKENS ensures that tokens are required to access the content matching the path.

2. Child manifest file block

- description: ChildPlayList
matchRules:
- pathTemplateMatch: /**.m3u8
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 2
routeAction:
cdnPolicy:
addSignatures:
actions:
- PROPAGATE_TOKEN_HLS_COOKIELESS
tokenQueryParameter: hdntl
signedRequestMode: REQUIRE_TOKENS
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: longkeyset
signedTokenOptions:
tokenQueryParameter: hdntl
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s

In this block, we are using longkeyset to validate the tokens and propagating tokens forward to ts chunk URLs. Hence the directive PROPAGATE_TOKEN_HLS_COOKIELESS. REQUIRE_TOKENS ensures that tokens are required to access the content matching the path. We are adding CORS headers towards the end

3. ts chunk files match block

- description: tschunks
matchRules:
- pathTemplateMatch: /**.ts
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 3
routeAction:
cdnPolicy:
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: longkeyset
signedRequestMode: REQUIRE_TOKENS
signedTokenOptions:
tokenQueryParameter: hdntl
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s

In this block, we are simply validating the long tokens, and adding CORS headers. REQUIRE_TOKENS ensures that tokens are required to access the content matching the path.

The complete YAML file will look like the following:

edgeSslCertificates:
- projects/my-media-project/locations/global/certificates/my-cert
logConfig:
enable: true
sampleRate: 1.0
name: projects/my-media-project/locations/global/edgeCacheServices/dual-sha256-cookieless
requireTls: true
routing:
hostRules:
- hosts:
- vod.cdntest.in
pathMatcher: path-matcher-0
pathMatchers:
- name: path-matcher-0
routeRules:
- description: DualTokenPOC
headerAction:
responseHeadersToAdd:
- headerName: version
headerValue: '5'
- headerName: x-cache-status
headerValue: '{cdn_cache_status}'
- headerName: x-client-city
headerValue: '{client_city}'
matchRules:
- pathTemplateMatch: /**/manifest.m3u8
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 1
routeAction:
cdnPolicy:
addSignatures:
actions:
- GENERATE_TOKEN_HLS_COOKIELESS
copiedParameters:
- PathGlobs
- URLPrefix
keyset: longkeyset
tokenQueryParameter: hdntl
tokenTtl: 86400s
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: shortkeyset
signedRequestMaximumExpirationTtl: 60s
signedRequestMode: REQUIRE_TOKENS
signedTokenOptions:
allowedSignatureAlgorithms:
- HMAC_SHA_256
- ED25519
- HMAC_SHA1
tokenQueryParameter: hdnts
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s
- description: ChildPlayList
matchRules:
- pathTemplateMatch: /**.m3u8
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 2
routeAction:
cdnPolicy:
addSignatures:
actions:
- PROPAGATE_TOKEN_HLS_COOKIELESS
tokenQueryParameter: hdntl
signedRequestMode: REQUIRE_TOKENS
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: longkeyset
signedTokenOptions:
tokenQueryParameter: hdntl
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s
- description: tschunks
matchRules:
- pathTemplateMatch: /**.ts
origin: projects/12345678912/locations/global/edgeCacheOrigins/steel-origin
priority: 3
routeAction:
cdnPolicy:
cacheMode: CACHE_ALL_STATIC
signedRequestKeyset: longkeyset
signedRequestMode: REQUIRE_TOKENS
signedTokenOptions:
tokenQueryParameter: hdntl
corsPolicy:
allowHeaders:
- '*'
allowMethods:
- '*'
allowOrigins:
- '*'
exposeHeaders:
- '*'
maxAge: 600s

To keep thing simple for VOD streaming example, I have used the caching rule of the following. This uses the default Media CDN Caching rules

cacheMode: CACHE_ALL_STATIC

Now our YAML file is ready. Next step is to upload/import your YAML file.

gcloud edge-cache services import <Edge-Cache-Service-Name> --source=<edge-cache-service-name>.yaml

Once imported successfully, the setup is ready to deliver HLS streams protected by dual tokens. To generate the tokens, you may use the official code blocks documented here. I have also created a sample repository of Python/PHP/NodeJS codes, which will help you to created tokenized URLs. In case you simply want to test the stream, you may check here.

This has simple UI to generate and test your setup.

A sample Tokenized URL will look like the following

https://vod.my-test-demo-site.com/tearsofsteel/manifest.m3u8?hdnts=URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Expires=1716667856~hmac=420e493159701a4ebf74a680fa14452cdf74647d4f942324b8acfe6844f38894

You will see the following parameters being passed in the URL:

hdtns: It carries the short token
URLPrefix=This defines to what path parameter the tokens are valid for. The value is the base64 encoded URL
Expires=This is the EPOCH time of token expiry
hmac=This is the SHA256 hash of the all input parameters, including your secret.

Sample tokenized master manifest file

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=7260309,AVERAGE-BANDWIDTH=6084687,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.29"
1080highp-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=4862181,AVERAGE-BANDWIDTH=4086210,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.5"
1080lowp-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=3119296,AVERAGE-BANDWIDTH=2627663,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.5"
720highpp-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=2374314,AVERAGE-BANDWIDTH=2017560,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720lowp-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=1799536,AVERAGE-BANDWIDTH=1535912,RESOLUTION=1024x576,CODECS="avc1.64001f,mp4a.40.2"
576p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=1479936,AVERAGE-BANDWIDTH=1259640,RESOLUTION=854x480,CODECS="avc1.64001e,mp4a.40.2"
480p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=891370,AVERAGE-BANDWIDTH=770835,RESOLUTION=640x360,CODECS="avc1.4d001e,mp4a.40.2"
360p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=598592,AVERAGE-BANDWIDTH=524860,RESOLUTION=428x240,CODECS="avc1.4d0015,mp4a.40.2"
240p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=438416,AVERAGE-BANDWIDTH=315310,RESOLUTION=256x144,CODECS="avc1.64000c,mp4a.40.2"
144p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXT-X-STREAM-INF:BANDWIDTH=284005,AVERAGE-BANDWIDTH=216049,RESOLUTION=170x96,CODECS="avc1.64000b,mp4a.40.2"
96p-ts.m3u8?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8=~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J

A sample Tokenized child manifest file:

#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-VERSION:4
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.000,
#EXT-X-BYTERANGE:143820@0
360p-ts0000000000.ts?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXTINF:6.000,
#EXT-X-BYTERANGE:471128@143820
360p-ts0000000000.ts?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXTINF:6.000,
#EXT-X-BYTERANGE:600660@614948
360p-ts0000000000.ts?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXTINF:6.000,
#EXT-X-BYTERANGE:537868@1215608
360p-ts0000000000.ts?hdntl=Expires=1716754424~_GO=Generated~URLPrefix=aHR0cHM6Ly92b2QubXktdGVzdC1kZW1vLXNpdGUuY29tL3RlYXJzb2ZzdGVlbC8~Signature=AXPepaKLB5lKNX18ApUSBXbAa-CKPQYT9iGWmaV904AEa1kaXIOLp3CAnvFYcwYbZmnIYpNjlO_tIb4e07dLLyumUI8J
#EXTINF:6.000,

Hope you now understand how to create Media CDN setup, and have all the required resources to securely deliver your HLS media streaming content.

--

--

Vivek Sinha Anurag
Google Cloud - Community

Customer Engineer- CDN & Network Specialist @google, ex- @aws, @akamai, @bitgravity. In love with CDN, Media streaming, Network & Security. All views are my own