Release Argo Tunnel Client 2018.7.0

This commit is contained in:
cloudflare-warp-bot 2018-07-05 19:06:27 +00:00
parent 4268bc1a9c
commit 7f822deed9
28 changed files with 3209 additions and 178 deletions

View File

@ -195,9 +195,9 @@ func getOriginCert(c *cli.Context) ([]byte, error) {
If the path above is wrong, specify the path with the -origincert option.
If you don't have a certificate signed by Cloudflare, run the command:
%s login
%s login
`, originCertPath, os.Args[0])
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
}
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
@ -257,7 +257,7 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, pr
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool},
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
}
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
@ -265,29 +265,31 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, pr
}
return &origin.TunnelConfig{
EdgeAddrs: c.StringSlice("edge"),
OriginUrl: url,
Hostname: hostname,
OriginCert: originCert,
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
ClientTlsConfig: httpTransport.TLSClientConfig,
Retries: c.Uint("retries"),
HeartbeatInterval: c.Duration("heartbeat-interval"),
MaxHeartbeats: c.Uint64("heartbeat-count"),
ClientID: clientID,
BuildInfo: buildInfo,
ReportedVersion: Version,
LBPool: c.String("lb-pool"),
Tags: tags,
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
ProtocolLogger: protoLogger,
Logger: logger,
IsAutoupdated: c.Bool("is-autoupdated"),
GracePeriod: c.Duration("grace-period"),
RunFromTerminal: isRunningFromTerminal(),
EdgeAddrs: c.StringSlice("edge"),
OriginUrl: url,
Hostname: hostname,
OriginCert: originCert,
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
ClientTlsConfig: httpTransport.TLSClientConfig,
Retries: c.Uint("retries"),
HeartbeatInterval: c.Duration("heartbeat-interval"),
MaxHeartbeats: c.Uint64("heartbeat-count"),
ClientID: clientID,
BuildInfo: buildInfo,
ReportedVersion: Version,
LBPool: c.String("lb-pool"),
Tags: tags,
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
ProtocolLogger: protoLogger,
Logger: logger,
IsAutoupdated: c.Bool("is-autoupdated"),
GracePeriod: c.Duration("grace-period"),
RunFromTerminal: isRunningFromTerminal(),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
CompressionQuality: c.Uint64("compression-quality"),
}, nil
}

View File

@ -18,7 +18,7 @@ import (
cli "gopkg.in/urfave/cli.v2"
)
const baseLoginURL = "https://www.cloudflare.com/a/warp"
const baseLoginURL = "https://dash.cloudflare.com/warp"
const baseCertStoreURL = "https://login.cloudflarewarp.com"
const clientTimeout = time.Minute * 20

View File

@ -95,6 +95,11 @@ func main() {
EnvVars: []string{"TUNNEL_CACERT"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-tls-verify",
Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
EnvVars: []string{"NO_TLS_VERIFY"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origincert",
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
@ -273,19 +278,31 @@ func main() {
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period",
Usage: "Duration to accpet new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
Value: time.Second * 30,
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
Hidden: true,
}),
altsrc.NewUintFlag(&cli.UintFlag{
Name: "compression-quality",
Value: 0,
Usage: "Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high",
EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-chunked-encoding",
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
}),
}
app.Action = func(c *cli.Context) (err error) {
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanicAndWait(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
if err != nil {
raven.CaptureErrorAndWait(err, nil)
raven.CaptureError(err, nil)
}
return err
}

View File

@ -8,7 +8,7 @@ type AtomicCounter struct {
count uint64
}
func NewAtomicCounter(initCount uint64) *AtomicCounter{
func NewAtomicCounter(initCount uint64) *AtomicCounter {
return &AtomicCounter{count: initCount}
}
@ -16,7 +16,12 @@ func (c *AtomicCounter) IncrementBy(number uint64) {
atomic.AddUint64(&c.count, number)
}
// Get returns the current value of counter and reset it to 0
// Count returns the current value of counter and reset it to 0
func (c *AtomicCounter) Count() uint64 {
return atomic.SwapUint64(&c.count, 0)
}
// Value returns the current value of counter
func (c *AtomicCounter) Value() uint64 {
return atomic.LoadUint64(&c.count)
}

View File

@ -0,0 +1,21 @@
// +build cgo
package h2mux
import (
"io"
"code.cfops.it/go/brotli"
)
func CompressionIsSupported() bool {
return true
}
func newDecompressor(src io.Reader) *brotli.Reader {
return brotli.NewReader(src)
}
func newCompressor(dst io.Writer, quality, lgwin int) *brotli.Writer {
return brotli.NewWriter(dst, brotli.WriterOptions{Quality: quality, LGWin: lgwin})
}

View File

@ -0,0 +1,19 @@
// +build !cgo
package h2mux
import (
"io"
)
func CompressionIsSupported() bool {
return false
}
func newDecompressor(src io.Reader) decompressor {
return nil
}
func newCompressor(dst io.Writer, quality, lgwin int) compressor {
return nil
}

593
h2mux/h2_dictionaries.go Normal file
View File

@ -0,0 +1,593 @@
package h2mux
import (
"bytes"
"io"
"strings"
"sync"
"golang.org/x/net/http2"
)
/* This is an implementation of https://github.com/vkrasnov/h2-compression-dictionaries
but modified for tunnels in a few key ways:
Since tunnels is a server-to-server service, some aspects of the spec would cause
unnessasary head-of-line blocking on the CPU and on the network, hence this implementation
allows for parallel compression on the "client", and buffering on the "server" to solve
this problem. */
// Assign temporary values
const SettingCompression http2.SettingID = 0xff20
const (
FrameSetCompressionContext http2.FrameType = 0xf0
FrameUseDictionary http2.FrameType = 0xf1
FrameSetDictionary http2.FrameType = 0xf2
)
const (
FlagSetDictionaryAppend http2.Flags = 0x1
FlagSetDictionaryOffset http2.Flags = 0x2
)
const compressionVersion = uint8(1)
const compressionFormat = uint8(2)
type CompressionSetting uint
const (
CompressionNone CompressionSetting = iota
CompressionLow
CompressionMedium
CompressionMax
)
type CompressionPreset struct {
nDicts, dictSize, quality uint8
}
type compressor interface {
Write([]byte) (int, error)
Flush() error
SetDictionary([]byte)
Close() error
}
type decompressor interface {
Read([]byte) (int, error)
SetDictionary([]byte)
Close() error
}
var compressionPresets = map[CompressionSetting]CompressionPreset{
CompressionNone: {0, 0, 0},
CompressionLow: {32, 17, 5},
CompressionMedium: {64, 18, 6},
CompressionMax: {255, 19, 9},
}
func compressionSettingVal(version, fmt, sz, nd uint8) uint32 {
// Currently the compression settings are inlcude:
// * version: only 1 is supported
// * fmt: only 2 for brotli is supported
// * sz: log2 of the maximal allowed dictionary size
// * nd: max allowed number of dictionaries
return uint32(version)<<24 + uint32(fmt)<<16 + uint32(sz)<<8 + uint32(nd)
}
func parseCompressionSettingVal(setting uint32) (version, fmt, sz, nd uint8) {
version = uint8(setting >> 24)
fmt = uint8(setting >> 16)
sz = uint8(setting >> 8)
nd = uint8(setting)
return
}
func (c CompressionSetting) toH2Setting() uint32 {
p, ok := compressionPresets[c]
if !ok {
return 0
}
return compressionSettingVal(compressionVersion, compressionFormat, p.dictSize, p.nDicts)
}
func (c CompressionSetting) getPreset() CompressionPreset {
return compressionPresets[c]
}
type dictUpdate struct {
reader *h2DictionaryReader
dictionary *h2ReadDictionary
buff []byte
isReady bool
isUse bool
s setDictRequest
}
type h2ReadDictionary struct {
dictionary []byte
queue []*dictUpdate
maxSize int
}
type h2ReadDictionaries struct {
d []h2ReadDictionary
maxSize int
}
type h2DictionaryReader struct {
*SharedBuffer // Propagate the decompressed output into the original buffer
decompBuffer *bytes.Buffer // Intermediate buffer for the brotli compressor
dictionary []byte // The content of the dictionary being used by this reader
internalBuffer []byte
s, e int // Start and end of the buffer
decomp decompressor // The brotli compressor
isClosed bool // Indicates that Close was called for this reader
queue []*dictUpdate // List of dictionaries to update, when the data is available
}
type h2WriteDictionary []byte
type setDictRequest struct {
streamID uint32
dictID uint8
dictSZ uint64
truncate, offset uint64
P, E, D bool
}
type useDictRequest struct {
dictID uint8
streamID uint32
setDict []setDictRequest
}
type h2WriteDictionaries struct {
dictLock sync.Mutex
dictChan chan useDictRequest
dictionaries []h2WriteDictionary
nextAvail int // next unused dictionary slot
maxAvail int // max ID, defined by SETTINGS
maxSize int // max size, defined by SETTINGS
typeToDict map[string]uint8 // map from content type to dictionary that encodes it
pathToDict map[string]uint8 // map from path to dictionary that encodes it
quality int
window int
compIn, compOut *AtomicCounter
}
type h2DictWriter struct {
*bytes.Buffer
comp compressor
dicts *h2WriteDictionaries
writerLock sync.Mutex
streamID uint32
path string
contentType string
}
type h2Dictionaries struct {
write *h2WriteDictionaries
read *h2ReadDictionaries
}
func (o *dictUpdate) update(buff []byte) {
o.buff = make([]byte, len(buff))
copy(o.buff, buff)
o.isReady = true
}
func (d *h2ReadDictionary) update() {
for len(d.queue) > 0 {
o := d.queue[0]
if !o.isReady {
break
}
if o.isUse {
reader := o.reader
reader.dictionary = make([]byte, len(d.dictionary))
copy(reader.dictionary, d.dictionary)
reader.decomp = newDecompressor(reader.decompBuffer)
if len(reader.dictionary) > 0 {
reader.decomp.SetDictionary(reader.dictionary)
}
reader.Write([]byte{})
} else {
d.dictionary = adjustDictionary(d.dictionary, o.buff, o.s, d.maxSize)
}
d.queue = d.queue[1:]
}
}
func newH2ReadDictionaries(nd, sz uint8) h2ReadDictionaries {
d := make([]h2ReadDictionary, int(nd))
for i := range d {
d[i].maxSize = 1 << uint(sz)
}
return h2ReadDictionaries{d: d, maxSize: 1 << uint(sz)}
}
func (dicts *h2ReadDictionaries) getDictByID(dictID uint8) (*h2ReadDictionary, error) {
if int(dictID) > len(dicts.d) {
return nil, MuxerStreamError{"dictID too big", http2.ErrCodeProtocol}
}
return &dicts.d[dictID], nil
}
func (dicts *h2ReadDictionaries) newReader(b *SharedBuffer, dictID uint8) *h2DictionaryReader {
if int(dictID) > len(dicts.d) {
return nil
}
dictionary := &dicts.d[dictID]
reader := &h2DictionaryReader{SharedBuffer: b, decompBuffer: &bytes.Buffer{}, internalBuffer: make([]byte, dicts.maxSize)}
if len(dictionary.queue) == 0 {
reader.dictionary = make([]byte, len(dictionary.dictionary))
copy(reader.dictionary, dictionary.dictionary)
reader.decomp = newDecompressor(reader.decompBuffer)
if len(reader.dictionary) > 0 {
reader.decomp.SetDictionary(reader.dictionary)
}
} else {
dictionary.queue = append(dictionary.queue, &dictUpdate{isUse: true, isReady: true, reader: reader})
}
return reader
}
func (r *h2DictionaryReader) updateWaitingDictionaries() {
// Update all the waiting dictionaries
for _, o := range r.queue {
if o.isReady {
continue
}
if r.isClosed || uint64(r.e) >= o.s.dictSZ {
o.update(r.internalBuffer[:r.e])
if o == o.dictionary.queue[0] {
defer o.dictionary.update()
}
}
}
}
// Write actually happens when reading from network, this is therefore the stage where we decompress the buffer
func (r *h2DictionaryReader) Write(p []byte) (n int, err error) {
// Every write goes into brotli buffer first
n, err = r.decompBuffer.Write(p)
if err != nil {
return
}
if r.decomp == nil {
return
}
for {
m, err := r.decomp.Read(r.internalBuffer[r.e:])
if err != nil && err != io.EOF {
r.SharedBuffer.Close()
r.decomp.Close()
return n, err
}
r.SharedBuffer.Write(r.internalBuffer[r.e : r.e+m])
r.e += m
if m == 0 {
break
}
if r.e == len(r.internalBuffer) {
r.updateWaitingDictionaries()
r.e = 0
}
}
r.updateWaitingDictionaries()
if r.isClosed {
r.SharedBuffer.Close()
r.decomp.Close()
}
return
}
func (r *h2DictionaryReader) Close() error {
if r.isClosed {
return nil
}
r.isClosed = true
r.Write([]byte{})
return nil
}
var compressibleTypes = map[string]bool{
"application/atom+xml": true,
"application/javascript": true,
"application/json": true,
"application/ld+json": true,
"application/manifest+json": true,
"application/rss+xml": true,
"application/vnd.geo+json": true,
"application/vnd.ms-fontobject": true,
"application/x-font-ttf": true,
"application/x-yaml": true,
"application/x-web-app-manifest+json": true,
"application/xhtml+xml": true,
"application/xml": true,
"font/opentype": true,
"image/bmp": true,
"image/svg+xml": true,
"image/x-icon": true,
"text/cache-manifest": true,
"text/css": true,
"text/html": true,
"text/plain": true,
"text/vcard": true,
"text/vnd.rim.location.xloc": true,
"text/vtt": true,
"text/x-component": true,
"text/x-cross-domain-policy": true,
"text/x-yaml": true,
}
func getContentType(headers []Header) string {
for _, h := range headers {
if strings.ToLower(h.Name) == "content-type" {
val := strings.ToLower(h.Value)
sep := strings.IndexRune(val, ';')
if sep != -1 {
return val[:sep]
}
return val
}
}
return ""
}
func newH2WriteDictionaries(nd, sz, quality uint8, compIn, compOut *AtomicCounter) (*h2WriteDictionaries, chan useDictRequest) {
useDictChan := make(chan useDictRequest)
return &h2WriteDictionaries{
dictionaries: make([]h2WriteDictionary, nd),
nextAvail: 0,
maxAvail: int(nd),
maxSize: 1 << uint(sz),
dictChan: useDictChan,
typeToDict: make(map[string]uint8),
pathToDict: make(map[string]uint8),
quality: int(quality),
window: 1 << uint(sz+1),
compIn: compIn,
compOut: compOut,
}, useDictChan
}
func adjustDictionary(currentDictionary, newData []byte, set setDictRequest, maxSize int) []byte {
currentDictionary = append(currentDictionary, newData[:set.dictSZ]...)
if len(currentDictionary) > maxSize {
currentDictionary = currentDictionary[len(currentDictionary)-maxSize:]
}
return currentDictionary
}
func (h2d *h2WriteDictionaries) getNextDictID() (dictID uint8, ok bool) {
if h2d.nextAvail < h2d.maxAvail {
dictID, ok = uint8(h2d.nextAvail), true
h2d.nextAvail++
return
}
return 0, false
}
func (h2d *h2WriteDictionaries) getGenericDictID() (dictID uint8, ok bool) {
if h2d.maxAvail == 0 {
return 0, false
}
return uint8(h2d.maxAvail - 1), true
}
func (h2d *h2WriteDictionaries) getDictWriter(s *MuxedStream, headers []Header) *h2DictWriter {
w := s.writeBuffer
if w == nil {
return nil
}
if s.method != "GET" && s.method != "POST" {
return nil
}
s.contentType = getContentType(headers)
if _, ok := compressibleTypes[s.contentType]; !ok && !strings.HasPrefix(s.contentType, "text") {
return nil
}
return &h2DictWriter{
Buffer: w.(*bytes.Buffer),
path: s.path,
contentType: s.contentType,
streamID: s.streamID,
dicts: h2d,
}
}
func assignDictToStream(s *MuxedStream, p []byte) bool {
// On first write to stream:
// * assign the right dictionary
// * update relevant dictionaries
// * send the required USE_DICT and SET_DICT frames
h2d := s.dictionaries.write
if h2d == nil {
return false
}
w, ok := s.writeBuffer.(*h2DictWriter)
if !ok || w.comp != nil {
return false
}
h2d.dictLock.Lock()
if w.comp != nil {
// Check again with lock, in therory the inteface allows for unordered writes
h2d.dictLock.Unlock()
return false
}
// The logic of dictionary generation is below
// Is there a dictionary for the exact path or content-type?
var useID uint8
pathID, pathFound := h2d.pathToDict[w.path]
typeID, typeFound := h2d.typeToDict[w.contentType]
if pathFound {
// Use dictionary for path as top priority
useID = pathID
if !typeFound { // Shouldn't really happen, unless type changes between requests
typeID, typeFound = h2d.getNextDictID()
if typeFound {
h2d.typeToDict[w.contentType] = typeID
}
}
} else if typeFound {
// Use dictionary for same content type as second priority
useID = typeID
pathID, pathFound = h2d.getNextDictID()
if pathFound { // If a slot is available, generate new dictionary for path
h2d.pathToDict[w.path] = pathID
}
} else {
// Use the overflow dictionary as last resort
// If slots are availabe generate new dictioanries for path and content-type
useID, _ = h2d.getGenericDictID()
pathID, pathFound = h2d.getNextDictID()
if pathFound {
h2d.pathToDict[w.path] = pathID
}
typeID, typeFound = h2d.getNextDictID()
if typeFound {
h2d.typeToDict[w.contentType] = typeID
}
}
useLen := h2d.maxSize
if len(p) < useLen {
useLen = len(p)
}
// Update all the dictionaries using the new data
setDicts := make([]setDictRequest, 0, 3)
setDict := setDictRequest{
streamID: w.streamID,
dictID: useID,
dictSZ: uint64(useLen),
}
setDicts = append(setDicts, setDict)
if pathID != useID {
setDict.dictID = pathID
setDicts = append(setDicts, setDict)
}
if typeID != useID {
setDict.dictID = typeID
setDicts = append(setDicts, setDict)
}
h2d.dictChan <- useDictRequest{streamID: w.streamID, dictID: uint8(useID), setDict: setDicts}
dict := h2d.dictionaries[useID]
// Brolti requires the dictionary to be immutable
copyDict := make([]byte, len(dict))
copy(copyDict, dict)
for _, set := range setDicts {
h2d.dictionaries[set.dictID] = adjustDictionary(h2d.dictionaries[set.dictID], p, set, h2d.maxSize)
}
w.comp = newCompressor(w.Buffer, h2d.quality, h2d.window)
s.writeLock.Lock()
h2d.dictLock.Unlock()
if len(copyDict) > 0 {
w.comp.SetDictionary(copyDict)
}
return true
}
func (w *h2DictWriter) Write(p []byte) (n int, err error) {
bufLen := w.Buffer.Len()
if w.comp != nil {
n, err = w.comp.Write(p)
if err != nil {
return
}
err = w.comp.Flush()
w.dicts.compIn.IncrementBy(uint64(n))
w.dicts.compOut.IncrementBy(uint64(w.Buffer.Len() - bufLen))
return
}
return w.Buffer.Write(p)
}
func (w *h2DictWriter) Close() error {
return w.comp.Close()
}
// From http2/hpack
func http2ReadVarInt(n byte, p []byte) (remain []byte, v uint64, err error) {
if n < 1 || n > 8 {
panic("bad n")
}
if len(p) == 0 {
return nil, 0, MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
}
v = uint64(p[0])
if n < 8 {
v &= (1 << uint64(n)) - 1
}
if v < (1<<uint64(n))-1 {
return p[1:], v, nil
}
origP := p
p = p[1:]
var m uint64
for len(p) > 0 {
b := p[0]
p = p[1:]
v += uint64(b&127) << m
if b&128 == 0 {
return p, v, nil
}
m += 7
if m >= 63 {
return origP, 0, MuxerStreamError{"invalid integer", http2.ErrCodeProtocol}
}
}
return nil, 0, MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
}
func appendVarInt(dst []byte, n byte, i uint64) []byte {
k := uint64((1 << n) - 1)
if i < k {
return append(dst, byte(i))
}
dst = append(dst, byte(k))
i -= k
for ; i >= 128; i >>= 7 {
dst = append(dst, byte(0x80|(i&0x7f)))
}
return append(dst, byte(i))
}

View File

@ -1,6 +1,7 @@
package h2mux
import (
"bytes"
"context"
"io"
"strings"
@ -47,7 +48,8 @@ type MuxerConfig struct {
// The minimum number of heartbeats to send before terminating the connection.
MaxHeartbeats uint64
// Logger to use
Logger *log.Entry
Logger *log.Entry
CompressionQuality CompressionSetting
}
type Muxer struct {
@ -78,6 +80,8 @@ type Muxer struct {
// explicitShutdown records whether the Muxer is closing because Shutdown was called, or due to another
// error.
explicitShutdown *BooleanFuse
compressionQuality CompressionPreset
}
type Header struct {
@ -109,10 +113,18 @@ func Handshake(
readyList: NewReadyList(),
streams: newActiveStreamMap(config.IsClient),
}
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
// Initialise the settings to identify this connection and confirm the other end is sane.
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
compressionSetting := http2.Setting{ID: SettingCompression, Val: config.CompressionQuality.toH2Setting()}
if CompressionIsSupported() {
log.Debug("Compression is supported")
m.compressionQuality = config.CompressionQuality.getPreset()
} else {
log.Debug("Compression is not supported")
}
expectedMagic := MuxerMagicOrigin
if config.IsClient {
handshakeSetting.Val = MuxerMagicOrigin
@ -120,7 +132,7 @@ func Handshake(
}
errChan := make(chan error, 2)
// Simultaneously send our settings and verify the peer's settings.
go func() { errChan <- m.f.WriteSettings(handshakeSetting) }()
go func() { errChan <- m.f.WriteSettings(handshakeSetting, compressionSetting) }()
go func() { errChan <- m.readPeerSettings(expectedMagic) }()
err := joinErrorsWithTimeout(errChan, 2, config.Timeout, ErrHandshakeTimeout)
if err != nil {
@ -197,6 +209,9 @@ func Handshake(
updateOutBoundBytesChan: updateOutBoundBytesChan,
}
m.muxWriter.headerEncoder = hpack.NewEncoder(&m.muxWriter.headerBuffer)
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
m.muxMetricsUpdater = newMuxMetricsUpdater(
updateRTTChan,
updateReceiveWindowChan,
@ -204,7 +219,24 @@ func Handshake(
updateInBoundBytesChan,
updateOutBoundBytesChan,
m.abortChan,
compBytesBefore,
compBytesAfter,
)
if m.compressionQuality.dictSize > 0 && m.compressionQuality.nDicts > 0 {
nd, sz := m.compressionQuality.nDicts, m.compressionQuality.dictSize
writeDicts, dictChan := newH2WriteDictionaries(
nd,
sz,
m.compressionQuality.quality,
compBytesBefore,
compBytesAfter,
)
readDicts := newH2ReadDictionaries(nd, sz)
m.muxReader.dictionaries = h2Dictionaries{read: &readDicts, write: writeDicts}
m.muxWriter.useDictChan = dictChan
}
return m, nil
}
@ -227,6 +259,23 @@ func (m *Muxer) readPeerSettings(magic uint32) error {
if magic != peerMagic {
return ErrBadHandshakeWrongMagic
}
peerCompression, ok := settingsFrame.Value(SettingCompression)
if !ok {
m.compressionQuality = compressionPresets[CompressionNone]
return nil
}
ver, fmt, sz, nd := parseCompressionSettingVal(peerCompression)
if ver != compressionVersion || fmt != compressionFormat || sz == 0 || nd == 0 {
m.compressionQuality = compressionPresets[CompressionNone]
return nil
}
// Values used for compression are the mimimum between the two peers
if sz < m.compressionQuality.dictSize {
m.compressionQuality.dictSize = sz
}
if nd < m.compressionQuality.nDicts {
m.compressionQuality.nDicts = nd
}
return nil
}
@ -328,13 +377,16 @@ func (m *Muxer) OpenStream(headers []Header, body io.Reader) (*MuxedStream, erro
stream := &MuxedStream{
responseHeadersReceived: make(chan struct{}),
readBuffer: NewSharedBuffer(),
writeBuffer: &bytes.Buffer{},
receiveWindow: defaultWindowSize,
receiveWindowCurrentMax: defaultWindowSize, // Initial window size limit. exponentially increase it when receiveWindow is exhausted
receiveWindowMax: maxWindowSize,
sendWindow: defaultWindowSize,
readyList: m.readyList,
writeHeaders: headers,
dictionaries: m.muxReader.dictionaries,
}
select {
// Will be received by mux writer
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: body}:

View File

@ -10,6 +10,7 @@ import (
"net"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
@ -45,6 +46,17 @@ func NewDefaultMuxerPair() *DefaultMuxerPair {
}
}
func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
origin, edge := net.Pipe()
return &DefaultMuxerPair{
OriginMuxConfig: MuxerConfig{Timeout: time.Second, IsClient: true, Name: "origin", CompressionQuality: quality},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{Timeout: time.Second, IsClient: false, Name: "edge", CompressionQuality: quality},
EdgeConn: edge,
doneC: make(chan struct{}),
}
}
func (p *DefaultMuxerPair) Handshake(t *testing.T) {
edgeErrC := make(chan error)
originErrC := make(chan error)
@ -653,3 +665,274 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
// nothing to read
}
}
func TestMultipleStreamsWithDictionaries(t *testing.T) {
for q := CompressionNone; q <= CompressionMax; q++ {
muxPair := NewCompressedMuxerPair(q)
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
`"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">` +
`<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">` +
`<head>` +
` <title>Your page title here</title>` +
`</head>` +
`<body>` +
`<h1>Your major heading here</h1>` +
`<p>` +
`This is a regular text paragraph.` +
`</p>` +
`<ul>` +
` <li>` +
` First bullet of a bullet list.` +
` </li>` +
` <li>` +
` This is the <em>second</em> bullet.` +
` </li>` +
`</ul>` +
`</body>` +
`</html>`
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
var contentType string
var pathHeader Header
for _, h := range stream.Headers {
if h.Name == ":path" {
pathHeader = h
break
}
}
if pathHeader.Name != ":path" {
panic("Couldn't find :path header in test")
}
if strings.Contains(pathHeader.Value, "html") {
contentType = "text/html; charset=utf-8"
} else if strings.Contains(pathHeader.Value, "js") {
contentType = "application/javascript"
} else if strings.Contains(pathHeader.Value, "css") {
contentType = "text/css"
} else {
contentType = "img/gif"
}
stream.WriteHeaders([]Header{
Header{Name: "content-type", Value: contentType},
})
stream.Write([]byte(strings.Replace(htmlBody, "paragraph", pathHeader.Value, 1) + stream.Headers[5].Value))
return nil
})
muxPair.HandshakeAndServe(t)
var wg sync.WaitGroup
paths := []string{
"/html1",
"/html2?sa:ds",
"/html3",
"/css1",
"/html1",
"/html2?sa:ds",
"/html3",
"/css1",
"/css2",
"/css3",
"/js",
"/js",
"/js",
"/js2",
"/img2",
"/html1",
"/html2?sa:ds",
"/html3",
"/css1",
"/css2",
"/css3",
"/js",
"/js",
"/js",
"/js2",
"/img1",
}
wg.Add(len(paths))
for i, s := range paths {
go func(i int, path string) {
stream, err := muxPair.EdgeMux.OpenStream(
[]Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "https"},
{Name: ":authority", Value: "tunnel.otterlyadorable.co.uk"},
{Name: ":path", Value: path},
{Name: "cf-ray", Value: "378948953f044408-SFO-DOG"},
{Name: "idx", Value: strconv.Itoa(i)},
{Name: "accept-encoding", Value: "gzip, br"},
},
nil,
)
if err != nil {
t.Fatalf("error in OpenStream: %s", err)
}
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(i)
responseBody := make([]byte, len(expectBody)*2)
n, err := stream.Read(responseBody)
if err != nil {
log.Printf("error from (*MuxedStream).Read: %s", err)
t.Fatalf("error from (*MuxedStream).Read: %s", err)
}
if n != len(expectBody) {
log.Printf("expected response body to have %d bytes, got %d", len(expectBody), n)
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
}
if string(responseBody[:n]) != expectBody {
log.Printf("expected response body %s, got %s", expectBody, responseBody[:n])
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
}
wg.Done()
}(i, s)
time.Sleep(1 * time.Millisecond)
}
wg.Wait()
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
}
}
}
func sampleSiteHandler(stream *MuxedStream) error {
var contentType string
var pathHeader Header
for _, h := range stream.Headers {
if h.Name == ":path" {
pathHeader = h
break
}
}
if pathHeader.Name != ":path" {
panic("Couldn't find :path header in test")
}
if strings.Contains(pathHeader.Value, "html") {
contentType = "text/html; charset=utf-8"
} else if strings.Contains(pathHeader.Value, "js") {
contentType = "application/javascript"
} else if strings.Contains(pathHeader.Value, "css") {
contentType = "text/css"
} else {
contentType = "img/gif"
}
stream.WriteHeaders([]Header{
Header{Name: "content-type", Value: contentType},
})
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
b, _ := ioutil.ReadFile("./sample" + pathHeader.Value)
stream.Write(b)
log.Debugf("Wrote body for stream %s", pathHeader.Value)
return nil
}
func sampleSiteTest(t *testing.T, muxPair *DefaultMuxerPair, path string) {
stream, err := muxPair.EdgeMux.OpenStream(
[]Header{
{Name: ":method", Value: "GET"},
{Name: ":scheme", Value: "https"},
{Name: ":authority", Value: "tunnel.otterlyadorable.co.uk"},
{Name: ":path", Value: path},
{Name: "accept-encoding", Value: "br, gzip"},
{Name: "cf-ray", Value: "378948953f044408-SFO-DOG"},
},
nil,
)
if err != nil {
t.Fatalf("error in OpenStream: %s", err)
}
expectBody, _ := ioutil.ReadFile("./sample" + path)
responseBody := make([]byte, len(expectBody))
n, err := io.ReadFull(stream, responseBody)
log.Debugf("Got body for stream %s", path)
if err != nil {
t.Fatalf("error from (*MuxedStream).Read: %s", err)
}
if n != len(expectBody) {
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
}
if string(responseBody[:n]) != string(expectBody) {
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
}
}
func TestSampleSiteWithDictionaries(t *testing.T) {
for q := CompressionNone; q <= CompressionMax; q++ {
muxPair := NewCompressedMuxerPair(q)
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
muxPair.HandshakeAndServe(t)
var wg sync.WaitGroup
paths := []string{
"/index.html",
"/index2.html",
"/index1.html",
"/ghost-url.min.js",
"/jquery.fitvids.js",
"/index1.html",
"/index2.html",
"/index.html",
}
wg.Add(len(paths))
for _, s := range paths {
go func(path string) {
sampleSiteTest(t, muxPair, path)
wg.Done()
}(s)
}
wg.Wait()
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
}
}
}
func TestLongSiteWithDictionaries(t *testing.T) {
for q := CompressionNone; q <= CompressionMedium; q++ {
muxPair := NewCompressedMuxerPair(q)
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
muxPair.HandshakeAndServe(t)
var wg sync.WaitGroup
rand.Seed(time.Now().Unix())
paths := []string{
"/index.html",
"/index1.html",
"/index2.html",
"/ghost-url.min.js",
"/jquery.fitvids.js"}
tstLen := 1000
wg.Add(tstLen)
for i := 0; i < tstLen; i++ {
path := paths[rand.Int()%len(paths)]
go func(path string) {
sampleSiteTest(t, muxPair, path)
wg.Done()
}(path)
}
wg.Wait()
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 100*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
}
}
}

View File

@ -6,6 +6,17 @@ import (
"sync"
)
type ReadWriteLengther interface {
io.ReadWriter
Reset()
Len() int
}
type ReadWriteClosedCloser interface {
io.ReadWriteCloser
Closed() bool
}
type MuxedStream struct {
Headers []Header
@ -13,7 +24,7 @@ type MuxedStream struct {
responseHeadersReceived chan struct{}
readBuffer *SharedBuffer
readBuffer ReadWriteClosedCloser
receiveWindow uint32
// current window size limit. Exponentially increase it when it's exhausted
receiveWindowCurrentMax uint32
@ -25,7 +36,7 @@ type MuxedStream struct {
writeLock sync.Mutex
// The zero value for Buffer is an empty buffer ready to use.
writeBuffer bytes.Buffer
writeBuffer ReadWriteLengther
sendWindow uint32
@ -38,14 +49,31 @@ type MuxedStream struct {
sentEOF bool
// true if the peer sent us an EOF
receivedEOF bool
// dictionary that was used to compress the stream
receivedUseDict bool
method string
contentType string
path string
dictionaries h2Dictionaries
readBufferLock sync.RWMutex
}
func (s *MuxedStream) Read(p []byte) (n int, err error) {
if s.dictionaries.read != nil {
s.readBufferLock.RLock()
b := s.readBuffer
s.readBufferLock.RUnlock()
return b.Read(p)
}
return s.readBuffer.Read(p)
}
func (s *MuxedStream) Write(p []byte) (n int, err error) {
s.writeLock.Lock()
ok := assignDictToStream(s, p)
if !ok {
s.writeLock.Lock()
}
defer s.writeLock.Unlock()
if s.writeEOF {
return 0, io.EOF
@ -87,6 +115,9 @@ func (s *MuxedStream) CloseWrite() error {
return io.EOF
}
s.writeEOF = true
if c, ok := s.writeBuffer.(io.Closer); ok {
c.Close()
}
s.writeNotify()
return nil
}
@ -97,6 +128,15 @@ func (s *MuxedStream) WriteHeaders(headers []Header) error {
if s.writeHeaders != nil {
return ErrStreamHeadersSent
}
if s.dictionaries.write != nil {
dictWriter := s.dictionaries.write.getDictWriter(s, headers)
if dictWriter != nil {
s.writeBuffer = dictWriter
}
}
s.writeHeaders = headers
s.headersSent = false
s.writeNotify()
@ -210,7 +250,7 @@ func (s *MuxedStream) getChunk() *streamChunk {
}
// Copies at most s.sendWindow bytes
writeLen, _ := io.CopyN(&chunk.buffer, &s.writeBuffer, int64(s.sendWindow))
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
s.sendWindow -= uint32(writeLen)
s.receiveWindow += s.windowUpdate
s.windowUpdate = 0

View File

@ -1,6 +1,7 @@
package h2mux
import (
"bytes"
"io"
"testing"
@ -15,6 +16,7 @@ func TestFlowControlSingleStream(t *testing.T) {
stream := &MuxedStream{
responseHeadersReceived: make(chan struct{}),
readBuffer: NewSharedBuffer(),
writeBuffer: &bytes.Buffer{},
receiveWindow: testWindowSize,
receiveWindowCurrentMax: testWindowSize,
receiveWindowMax: testMaxWindowSize,

View File

@ -40,6 +40,8 @@ type muxMetricsUpdater struct {
updateOutBoundBytesChan <-chan uint64
// shutdownC is to signal the muxerMetricsUpdater to shutdown
abortChan <-chan struct{}
compBytesBefore, compBytesAfter *AtomicCounter
}
type MuxerMetrics struct {
@ -48,6 +50,14 @@ type MuxerMetrics struct {
ReceiveWindowMin, ReceiveWindowMax, SendWindowMin, SendWindowMax uint32
InBoundRateCurr, InBoundRateMin, InBoundRateMax uint64
OutBoundRateCurr, OutBoundRateMin, OutBoundRateMax uint64
CompBytesBefore, CompBytesAfter *AtomicCounter
}
func (m *MuxerMetrics) CompRateAve() float64 {
if m.CompBytesBefore.Value() == 0 {
return 1.
}
return float64(m.CompBytesAfter.Value()) / float64(m.CompBytesBefore.Value())
}
type roundTripMeasurement struct {
@ -68,9 +78,9 @@ type flowControlData struct {
}
type rate struct {
curr uint64
curr uint64
min, max uint64
lock sync.RWMutex
lock sync.RWMutex
}
func newMuxMetricsUpdater(
@ -80,6 +90,7 @@ func newMuxMetricsUpdater(
updateInBoundBytesChan <-chan uint64,
updateOutBoundBytesChan <-chan uint64,
abortChan <-chan struct{},
compBytesBefore, compBytesAfter *AtomicCounter,
) *muxMetricsUpdater {
return &muxMetricsUpdater{
rttData: newRTTData(),
@ -93,6 +104,8 @@ func newMuxMetricsUpdater(
updateInBoundBytesChan: updateInBoundBytesChan,
updateOutBoundBytesChan: updateOutBoundBytesChan,
abortChan: abortChan,
compBytesBefore: compBytesBefore,
compBytesAfter: compBytesAfter,
}
}
@ -103,6 +116,7 @@ func (updater *muxMetricsUpdater) Metrics() *MuxerMetrics {
m.SendWindowAve, m.SendWindowMin, m.SendWindowMax = updater.sendWindowData.metrics()
m.InBoundRateCurr, m.InBoundRateMin, m.InBoundRateMax = updater.inBoundRate.get()
m.OutBoundRateCurr, m.OutBoundRateMin, m.OutBoundRateMax = updater.outBoundRate.get()
m.CompBytesBefore, m.CompBytesAfter = updater.compBytesBefore, updater.compBytesAfter
return m
}

View File

@ -61,7 +61,7 @@ func TestFlowControlDataUpdate(t *testing.T) {
for i := 1; i <= dataPoints; i++ {
size := maxWindowSize - uint32(i)
f.update(size)
assert.Equal(t, max - uint32(1), f.max)
assert.Equal(t, max-uint32(1), f.max)
assert.Equal(t, size, f.min)
assert.Equal(t, i, f.queue.Len())
@ -94,12 +94,15 @@ func TestMuxMetricsUpdater(t *testing.T) {
updateOutBoundBytesChan := make(chan uint64)
abortChan := make(chan struct{})
errChan := make(chan error)
compBefore, compAfter := NewAtomicCounter(0), NewAtomicCounter(0)
m := newMuxMetricsUpdater(updateRTTChan,
updateReceiveWindowChan,
updateSendWindowChan,
updateInBoundBytesChan,
updateOutBoundBytesChan,
abortChan,
compBefore,
compAfter,
)
logger := log.NewEntry(log.New())

View File

@ -1,8 +1,10 @@
package h2mux
import (
"bytes"
"encoding/binary"
"io"
"net/url"
"time"
log "github.com/sirupsen/logrus"
@ -45,6 +47,8 @@ type MuxReader struct {
bytesRead *AtomicCounter
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
updateInBoundBytesChan chan<- uint64
// dictionaries holds the h2 cross-stream compression dictionaries
dictionaries h2Dictionaries
}
func (r *MuxReader) Shutdown() {
@ -125,6 +129,15 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
// consumes data and frees up space in flow-control windows
case *http2.WindowUpdateFrame:
err = r.updateStreamWindow(f)
case *http2.UnknownFrame:
switch f.Header().Type {
case FrameUseDictionary:
err = r.receiveUseDictionary(f)
case FrameSetDictionary:
err = r.receiveSetDictionary(f)
default:
err = ErrUnexpectedFrameType
}
default:
err = ErrUnexpectedFrameType
}
@ -139,11 +152,13 @@ func (r *MuxReader) newMuxedStream(streamID uint32) *MuxedStream {
return &MuxedStream{
streamID: streamID,
readBuffer: NewSharedBuffer(),
writeBuffer: &bytes.Buffer{},
receiveWindow: r.initialStreamWindow,
receiveWindowCurrentMax: r.initialStreamWindow,
receiveWindowMax: r.streamWindowMax,
sendWindow: r.initialStreamWindow,
readyList: r.readyList,
dictionaries: r.dictionaries,
}
}
@ -207,10 +222,23 @@ func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
return r.defaultStreamErrorHandler(err, frame.Header())
}
}
headers := make([]Header, len(frame.Fields))
for i, header := range frame.Fields {
headers[i].Name = header.Name
headers[i].Value = header.Value
headers := make([]Header, 0, len(frame.Fields))
for _, header := range frame.Fields {
switch header.Name {
case ":method":
stream.method = header.Value
case ":path":
u, err := url.Parse(header.Value)
if err == nil {
stream.path = u.Path
}
case "accept-encoding":
// remove accept-encoding if dictionaries are enabled
if r.dictionaries.write != nil {
continue
}
}
headers = append(headers, Header{Name: header.Name, Value: header.Value})
}
stream.Headers = headers
if frame.Header().Flags.Has(http2.FlagHeadersEndStream) {
@ -291,6 +319,140 @@ func (r *MuxReader) receiveGoAway(frame *http2.GoAwayFrame) error {
return nil
}
// Receive a USE_DICTIONARY from the peer. Setup dictionary for stream.
func (r *MuxReader) receiveUseDictionary(frame *http2.UnknownFrame) error {
payload := frame.Payload()
streamID := frame.StreamID
// Check frame is formatted properly
if len(payload) != 1 {
return r.streamError(streamID, http2.ErrCodeProtocol)
}
stream, err := r.getStreamForFrame(frame)
if err != nil {
return err
}
if stream.receivedUseDict == true || stream.dictionaries.read == nil {
return r.streamError(streamID, http2.ErrCodeInternal)
}
stream.receivedUseDict = true
dictID := payload[0]
dictReader := stream.dictionaries.read.newReader(stream.readBuffer.(*SharedBuffer), dictID)
if dictReader == nil {
return r.streamError(streamID, http2.ErrCodeInternal)
}
stream.readBufferLock.Lock()
stream.readBuffer = dictReader
stream.readBufferLock.Unlock()
return nil
}
// Receive a SET_DICTIONARY from the peer. Update dictionaries accordingly.
func (r *MuxReader) receiveSetDictionary(frame *http2.UnknownFrame) (err error) {
payload := frame.Payload()
flags := frame.Flags
stream, err := r.getStreamForFrame(frame)
if err != nil && err != ErrClosedStream {
return err
}
reader, ok := stream.readBuffer.(*h2DictionaryReader)
if !ok {
return r.streamError(frame.StreamID, http2.ErrCodeProtocol)
}
// A SetDictionary frame consists of several
// Dictionary-Entries that specify how existing dictionaries
// are to be updated using the current stream data
// +---------------+---------------+
// | Dictionary-Entry (+) ...
// +---------------+---------------+
for {
// Each Dictionary-Entry is formatted as follows:
// +-------------------------------+
// | Dictionary-ID (8) |
// +---+---------------------------+
// | P | Size (7+) |
// +---+---------------------------+
// | E?| D?| Truncate? (6+) |
// +---+---------------------------+
// | Offset? (8+) |
// +-------------------------------+
var size, truncate, offset uint64
var p, e, d bool
// Parse a single Dictionary-Entry
if len(payload) < 2 { // Must have at least id and size
return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
}
dictID := uint8(payload[0])
p = (uint8(payload[1]) >> 7) == 1
payload, size, err = http2ReadVarInt(7, payload[1:])
if err != nil {
return
}
if flags.Has(FlagSetDictionaryAppend) {
// Presence of FlagSetDictionaryAppend means we expect e, d and truncate
if len(payload) < 1 {
return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
}
e = (uint8(payload[0]) >> 7) == 1
d = (uint8((payload[0])>>6) & 1) == 1
payload, truncate, err = http2ReadVarInt(6, payload)
if err != nil {
return
}
}
if flags.Has(FlagSetDictionaryOffset) {
// Presence of FlagSetDictionaryOffset means we expect offset
if len(payload) < 1 {
return MuxerStreamError{"unexpected EOF", http2.ErrCodeProtocol}
}
payload, offset, err = http2ReadVarInt(8, payload)
if err != nil {
return
}
}
setdict := setDictRequest{streamID: stream.streamID,
dictID: dictID,
dictSZ: size,
truncate: truncate,
offset: offset,
P: p,
E: e,
D: d}
// Find the right dictionary
dict, err := r.dictionaries.read.getDictByID(dictID)
if err != nil {
return err
}
// Register a dictionary update order for the dictionary and reader
updateEntry := &dictUpdate{reader: reader, dictionary: dict, s: setdict}
dict.queue = append(dict.queue, updateEntry)
reader.queue = append(reader.queue, updateEntry)
// End of frame
if len(payload) == 0 {
break
}
}
return nil
}
// Receives header frames from a stream. A non-nil error is a connection error.
func (r *MuxReader) updateStreamWindow(frame *http2.WindowUpdateFrame) error {
stream, err := r.getStreamForFrame(frame)

View File

@ -48,6 +48,8 @@ type MuxWriter struct {
bytesWrote *AtomicCounter
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
updateOutBoundBytesChan chan<- uint64
useDictChan <-chan useDictRequest
}
type MuxedStreamRequest struct {
@ -156,6 +158,13 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
return err
}
w.idleTimer.MarkActive()
case useDict := <-w.useDictChan:
err := w.writeUseDictionary(useDict)
if err != nil {
logger.WithError(err).Warn("error writing use dictionary")
return err
}
w.idleTimer.MarkActive()
}
}
}
@ -260,3 +269,19 @@ func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error {
}
return err
}
func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error {
err := w.f.WriteRawFrame(FrameUseDictionary, 0, dictRequest.streamID, []byte{byte(dictRequest.dictID)})
if err != nil {
return err
}
payload := make([]byte, 0, 64)
for _, set := range dictRequest.setDict {
payload = append(payload, byte(set.dictID))
payload = appendVarInt(payload, 7, uint64(set.dictSZ))
payload = append(payload, 0x80) // E = 1, D = 0, Truncate = 0
}
err = w.f.WriteRawFrame(FrameSetDictionary, FlagSetDictionaryAppend, dictRequest.streamID, payload)
return err
}

1
h2mux/sample/ghost-url.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){"use strict";function a(a){var b,c=[];if(!a)return"";for(b in a)a.hasOwnProperty(b)&&(a[b]||a[b]===!1)&&c.push(b+"="+encodeURIComponent(a[b]));return c.length?"?"+c.join("&"):""}var b,c,d,e,f="https://cloudflare.ghost.io/ghost/api/v0.1/";d={api:function(){var d,e=Array.prototype.slice.call(arguments),g=f;return d=e.pop(),d&&"object"!=typeof d&&(e.push(d),d={}),d=d||{},d.client_id=b,d.client_secret=c,e.length&&e.forEach(function(a){g+=a.replace(/^\/|\/$/g,"")+"/"}),g+a(d)}},e=function(a){b=a.clientId?a.clientId:"",c=a.clientSecret?a.clientSecret:"",f=a.url?a.url:f.match(/{\{api-url}}/)?"":f},"undefined"!=typeof window&&(window.ghost=window.ghost||{},window.ghost.url=d,window.ghost.init=e),"undefined"!=typeof module&&(module.exports={url:d,init:e})}();

537
h2mux/sample/index.html Normal file
View File

@ -0,0 +1,537 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Cloudflare Blog</title>
<meta name="description" content="" />
<meta name="HandheldFriendly" content="True">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<link rel="shortcut icon" href="/assets/images/favicon.ico?v=b6cf3f99a6">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/assets/images/apple-touch-icon-57x57-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/assets/images/apple-touch-icon-72x72-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/assets/images/apple-touch-icon-114x114-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/assets/images/apple-touch-icon-144x144-precomposed.png?v=b6cf3f99a6" />
<link rel="stylesheet" type="text/css" href="/assets/css/screen.css?v=b6cf3f99a6" />
<!--[if lt IE 9]><link rel="stylesheet" type="text/css" href="/assets/css/ie.css?v=b6cf3f99a6" /><![endif]-->
<!--<link href="http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,700,300,600" rel="stylesheet" type="text/css">-->
<script>(function(G,o,O,g,l){G.GoogleAnalyticsObject=O;G[O]||(G[O]=function(){(G[O].q=G[O].q||[]).push(arguments)});G[O].l=+new Date;g=o.createElement('script'),l=o.scripts[0];g.src='//www.google-analytics.com/analytics.js';l.parentNode.insertBefore(g,l)}(this,document,'ga'));ga('create','UA-10218544-12', 'auto');ga('send','pageview')</script>
<link rel="canonical" href="http://blog.cloudflare.com/" />
<meta name="referrer" content="no-referrer-when-downgrade" />
<link rel="next" href="https://blog.cloudflare.com/page/2/" />
<meta property="og:site_name" content="Cloudflare Blog" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Cloudflare Blog" />
<meta property="og:url" content="http://blog.cloudflare.com/" />
<meta property="og:image" content="http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb-1.png" />
<meta property="article:publisher" content="https://www.facebook.com/Cloudflare" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Cloudflare Blog" />
<meta name="twitter:url" content="http://blog.cloudflare.com/" />
<meta name="twitter:image" content="http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb-1.png" />
<meta name="twitter:site" content="@cloudflare" />
<meta property="og:image:width" content="189" />
<meta property="og:image:height" content="47" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Website",
"publisher": {
"@type": "Organization",
"name": "Cloudflare Blog",
"logo": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb.png",
"width": 189,
"height": 47
}
},
"url": "https://blog.cloudflare.com/",
"image": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb-1.png",
"width": 189,
"height": 47
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "http://blog.cloudflare.com"
}
}
</script>
<script type="text/javascript" src="/shared/ghost-url.min.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
ghost.init({
clientId: "ghost-frontend",
clientSecret: "cf0df60d1ab4"
});
</script>
<meta name="generator" content="Ghost 0.11" />
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
ga('send', 'event', 'recruiting', 'jobscore-click', role, {
'transport': 'beacon',
'hitCallback': function(){document.location = url;}
});
}
</script>
<script type="text/javascript">
(function() {
var didInit = false;
function initMunchkin() {
if(didInit === false) {
didInit = true;
Munchkin.init('713-XSC-918');
}
}
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = '//munchkin.marketo.net/munchkin.js';
s.onreadystatechange = function() {
if (this.readyState == 'complete' || this.readyState == 'loaded') {
initMunchkin();
}
};
s.onload = initMunchkin;
document.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script>
var HTMLAttrToAdd = document.querySelector("html");
HTMLAttrToAdd.setAttribute("lang", "en");
</script>
<style>
table {
background-color: transparent;
}
td {
padding: 5px 1em;
}
pre {
max-height: 500px;
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>
.st-default-search-input {
font-family: Helvetica, Arial, "Lucida Grande", sans-serif;
font-size: 14px;
line-height: 16px;
font-weight: 400;
-moz-transition: opacity 0.2s;
-o-transition: opacity 0.2s;
-webkit-transition: opacity 0.2s;
transition: opacity 0.2s;
display: inline-block;
width: 190px;
height: 16px;
padding: 7px 11px 7px 28px;
border: 1px solid rgba(0, 0, 0, 0.25);
color: #444;
-moz-box-sizing: content-box;
box-sizing: content-box;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #fff 8px 8px no-repeat url("data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6%2BR8AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAG11AABzoAAA%2FN0AAINkAABw6AAA7GgAADA%2BAAAQkOTsmeoAAAESSURBVHjajNCxS9VRGMbxz71E4OwgoXPQxVEpXCI47%2BZqGP0LCoJO7UVD3QZzb3SwcHB7F3Uw3Zpd%2FAPCcJKG7Dj4u%2FK7Pwp94HDg5Xyf5z1Pr9YKImKANTzFXxzjU2ae6qhXaxURr%2FAFl9hHDy%2FwEK8z89sYVEp5gh84wMvMvGiSJ%2FEV85jNzLMR1McqfmN5BEBmnmMJFSvtpH7jdJiZv7q7Z%2BZPfMdcF6rN%2FT%2F1m2LGBkd4HhFT3dcRMY2FpskxaLNpayciHrWAGeziD7b%2BVfkithuTk8bkGa4wgWFmbrSTZOYeBvjc%2BucQj%2FEe6xHx4Taq1nrnKaW8K6XUUsrHWuvNevdRRLzFGwzvDbXAB9cDAHvhedDruuxSAAAAAElFTkSuQmCC")
}
.st-ui-close-button {
-moz-transition: none;
-o-transition: none;
-webkit-transition: none;
transition: none
}
</style>
</head>
<body class="home-template">
<div id="fb-root"></div>
<header id="header" class="header">
<div class="wrapper">
<a href="https://www.cloudflare.com" class="logo logo-header">Cloudflare</a>
<nav id="main-menu" class="header-navigation navigation" role="navigation">
<ul class="menu menu-header">
<li><a href="https://blog.cloudflare.com/">Blog home</a></li>
<li><a href="https://www.cloudflare.com/overview" tabindex="1">What we do</a></li>
<li><a href="https://www.cloudflare.com/support" tabindex="9">Support</a></li>
<li><a href="https://www.cloudflare.com/community" tabindex="9">Community</a></li>
<li><a href="https://www.cloudflare.com/login" tabindex="10">Login</a></li>
<li><a href="https://www.cloudflare.com/sign-up" class="btn btn-success" tabindex="11">Sign up</a></li>
</ul>
</nav>
</div>
</header>
<div class="wrapper reverse-sidebar">
<section class="primary-content" role="main">
<article class="post tag-google-cloud tag-cloud-computing tag-internet-summit">
<header class="post-header">
<h2 class="title"><a href="/living-in-a-multi-cloud-world/">Living In A Multi-Cloud World</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 21st, 2017 4:30PM">November 21st, 2017 4:30PM</time>
by <a href="/author/sergi/">Sergi Isasi</a>.
</div>
</header>
<div class="post-excerpt">
<p>A few months ago at Cloudflares Internet Summit, we hosted a discussion on A Cloud Without Handcuffs with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS. The conversation touched on multiple areas, but its clear that more and more companies are recognizing the need to have some strategy around hosting their applications on multiple cloud providers. Earlier this year,&hellip;</p>
</div>
<footer>
<a href="/living-in-a-multi-cloud-world/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/living-in-a-multi-cloud-world/#disqus_thread">Comments</a> | tagged with <a href="/tag/google-cloud/">Google Cloud</a>, <a href="/tag/cloud-computing/">Cloud Computing</a>, <a href="/tag/internet-summit/">Internet Summit</a>
</span>
</small>
</footer>
</article>
<article class="post tag-legal tag-jengo tag-patents">
<header class="post-header">
<h2 class="title"><a href="/supreme-court-wanders-into-patent-troll-fight/">The Supreme Court Wanders into the Patent Troll Fight</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 20th, 2017 6:18PM">November 20th, 2017 6:18PM</time>
by <a href="/author/edo-royker/">Edo Royker</a>.
</div>
</header>
<div class="post-excerpt">
<p>Next Monday, the US Supreme Court will hear oral arguments in Oil States Energy Services, LLC vs. Greenes Energy Group, LLC, which is a case to determine whether the Inter Partes Review (IPR) administrative process at the US Patent and Trademark Office (USPTO) used to determine the validity of patents is constitutional. The constitutionality of the IPR process is one of the biggest legal issues facing innovative&hellip;</p>
</div>
<footer>
<a href="/supreme-court-wanders-into-patent-troll-fight/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/supreme-court-wanders-into-patent-troll-fight/#disqus_thread">Comments</a> | tagged with <a href="/tag/legal/">Legal</a>, <a href="/tag/jengo/">Jengo</a>, <a href="/tag/patents/">Patents</a>
</span>
</small>
</footer>
</article>
<article class="post tag-cloudflare-apps tag-developers tag-user-engagement">
<header class="post-header">
<h2 class="title"><a href="/7cloudflareappsengagement/">7 Cloudflare Apps Which Increase User Engagement on Your Site</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 14th, 2017 8:21PM">November 14th, 2017 8:21PM</time>
by <a href="/author/andrew/">Andrew Fitch</a>.
</div>
</header>
<div class="post-excerpt">
<p>Cloudflare Apps now lists 95 apps from apps which grow email lists to apps which acquire new customers to apps which help site owners make more money. The great thing about these apps is that users don't have to have any coding or development skills. They can just sign up for the app and start using it on their sites. Lets take a moment to highlight some&hellip;</p>
</div>
<footer>
<a href="/7cloudflareappsengagement/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/7cloudflareappsengagement/#disqus_thread">Comments</a> | tagged with <a href="/tag/cloudflare-apps/">Cloudflare Apps</a>, <a href="/tag/developers/">Developers</a>, <a href="/tag/user-engagement/">User Engagement</a>
</span>
</small>
</footer>
</article>
<article class="post tag-acquisitions tag-cloudflare-team tag-mobile tag-neumob">
<header class="post-header">
<h2 class="title"><a href="/neumob-optimizing-mobile/">The Super Secret Cloudflare Master Plan, or why we acquired Neumob</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 14th, 2017 2:00PM">November 14th, 2017 2:00PM</time>
by <a href="/author/john-graham-cumming/">John Graham-Cumming</a>.
</div>
</header>
<div class="post-excerpt">
<p>We announced today that Cloudflare has acquired Neumob. Neumobs team built exceptional technology to speed up mobile apps, reduce errors on challenging mobile networks, and increase conversions. Cloudflare will integrate the Neumob technology with our global network to give Neumob truly global reach. Its tempting to think of the Neumob acquisition as a point product added to the Cloudflare portfolio. But it actually represents a key&hellip;</p>
</div>
<footer>
<a href="/neumob-optimizing-mobile/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/neumob-optimizing-mobile/#disqus_thread">Comments</a> | tagged with <a href="/tag/acquisitions/">Acquisitions</a>, <a href="/tag/cloudflare-team/">Cloudflare Team</a>, <a href="/tag/mobile/">Mobile</a>, <a href="/tag/neumob/">Neumob</a>
</span>
</small>
</footer>
</article>
<article class="post tag-security tag-legal tag-privacy tag-attacks">
<header class="post-header">
<h2 class="title"><a href="/thwarting-the-tactics-of-the-equifax-attackers/">Thwarting the Tactics of the Equifax Attackers</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 13th, 2017 4:09PM">November 13th, 2017 4:09PM</time>
by <a href="/author/alex-cruz-farmer/">Alex Cruz Farmer</a>.
</div>
</header>
<div class="post-excerpt">
<p>We are now 3 months on from one of the biggest, most significant data breaches in history, but has it redefined people's awareness on security? The answer to that is absolutely yes, awareness is at an all-time high. Awareness, however, does not always result in positive action. The fallacy which is often assumed is "surely, if I keep my software up to date with all the patches, that's&hellip;</p>
</div>
<footer>
<a href="/thwarting-the-tactics-of-the-equifax-attackers/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/thwarting-the-tactics-of-the-equifax-attackers/#disqus_thread">Comments</a> | tagged with <a href="/tag/security/">Security</a>, <a href="/tag/legal/">Legal</a>, <a href="/tag/privacy/">Privacy</a>, <a href="/tag/attacks/">Attacks</a>
</span>
</small>
</footer>
</article>
<article class="post tag-go tag-performance tag-golang tag-developers">
<header class="post-header">
<h2 class="title"><a href="/go-dont-collect-my-garbage/">Go, don&#x27;t collect my garbage</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 13th, 2017 10:31AM">November 13th, 2017 10:31AM</time>
by <a href="/author/vlad-krasnov/">Vlad Krasnov</a>.
</div>
</header>
<div class="post-excerpt">
<p>Not long ago I needed to benchmark the performance of Golang on a many-core machine. I took several of the benchmarks that are bundled with the Go source code, copied them, and modified them to run on all available threads. In that case the machine has 24 cores and 48 threads. CC BY-SA 2.0 image by sponki25 I started with ECDSA P256 Sign, probably because I have&hellip;</p>
</div>
<footer>
<a href="/go-dont-collect-my-garbage/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/go-dont-collect-my-garbage/#disqus_thread">Comments</a> | tagged with <a href="/tag/go/">Go</a>, <a href="/tag/performance/">Performance</a>, <a href="/tag/golang/">golang</a>, <a href="/tag/developers/">Developers</a>
</span>
</small>
</footer>
</article>
<article class="post tag-developers tag-javascript tag-php tag-lua tag-go tag-meetup tag-cloudflare-meetups tag-community tag-pizza">
<header class="post-header">
<h2 class="title"><a href="/cloudflare-wants-to-buy-your-meetup-group-pizza/">Cloudflare Wants to Buy Your Meetup Group Pizza</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 10th, 2017 3:00PM">November 10th, 2017 3:00PM</time>
by <a href="/author/andrew/">Andrew Fitch</a>.
</div>
</header>
<div class="post-excerpt">
<p>If youre a web dev / devops / etc. meetup group that also works toward building a faster, safer Internet, I want to support your awesome group by buying you pizza. If your groups focus falls within one of the subject categories below and youre willing to give us a 30 second shout out and tweet a photo of your group and @Cloudflare, your meetups pizza&hellip;</p>
</div>
<footer>
<a href="/cloudflare-wants-to-buy-your-meetup-group-pizza/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/cloudflare-wants-to-buy-your-meetup-group-pizza/#disqus_thread">Comments</a> | tagged with <a href="/tag/developers/">Developers</a>, <a href="/tag/javascript/">javascript</a>, <a href="/tag/php/">php</a>, <a href="/tag/lua/">lua</a>, <a href="/tag/go/">Go</a>, <a href="/tag/meetup/">MeetUp</a>, <a href="/tag/cloudflare-meetups/">Cloudflare Meetups</a>, <a href="/tag/community/">Community</a>, <a href="/tag/pizza/">Pizza</a>
</span>
</small>
</footer>
</article>
<article class="post">
<header class="post-header">
<h2 class="title"><a href="/on-the-dangers-of-intels-frequency-scaling/">On the dangers of Intel&#x27;s frequency scaling</a></h2>
<div class="meta">
Published on <time class="meta-date" datetime="November 10th, 2017 11:06AM">November 10th, 2017 11:06AM</time>
by <a href="/author/vlad-krasnov/">Vlad Krasnov</a>.
</div>
</header>
<div class="post-excerpt">
<p>While I was writing the post comparing the new Qualcomm server chip, Centriq, to our current stock of Intel Skylake-based Xeons, I noticed a disturbing phenomena. When benchmarking OpenSSL 1.1.1dev, I discovered that the performance of the cipher ChaCha20-Poly1305 does not scale very well. On a single thread, it performed at the speed of approximately 2.89GB/s, whereas on 24 cores, and 48 threads it&hellip;</p>
</div>
<footer>
<a href="/on-the-dangers-of-intels-frequency-scaling/" class="more">Read more &raquo; </a><br>
<small>
<span class="post-meta">
<a href="/on-the-dangers-of-intels-frequency-scaling/#disqus_thread">Comments</a>
</span>
</small>
</footer>
</article>
<section class="clearfix" role="navigation">
<a class="newer-posts btn" href="/page/2/">Older &raquo;</a>
</section>
</section>
<aside class="sidebar">
<div class="widget">
<input type="text" placeholder="Search the blog" class="st-default-search-input"></input>
<script type="text/javascript">
(function(w,d,t,u,n,s,e){w['SwiftypeObject']=n;w[n]=w[n]||function(){
(w[n].q=w[n].q||[]).push(arguments);};s=d.createElement(t);
e=d.getElementsByTagName(t)[0];s.async=0;s.src=u;e.parentNode.insertBefore(s,e);
})(window,document,'script','//s.swiftypecdn.com/install/v2/st.js','_st');
_st('install','_KobMC_zsd_tDx_7NWiX','2.0.0');
</script>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare blog</h4>
<p style="margin-top: 20px">
<a href="https://www.cloudflare.com/enterprise-service-request" class="btn btn-success" tabindex="11" target="_blank">Contact our team</a>
</p>
<p>
<strong>US callers</strong><br/>
1 (888) 99-FLARE <br/>
<strong>UK callers</strong><br/>
+44 (0)20 3514 6970<br/>
<strong>International callers</strong><br/>
+1 (650) 319-8930 <BR/><BR/>
<a href="https://www.cloudflare.com/plans" target="_blank">Full feature list and plan types</a>
</p>
<p>Cloudflare provides performance and security for any website. More than 6 million websites use Cloudflare.</p>
<p>There is no hardware or software. Cloudflare works at the DNS level. It takes only 5 minutes to sign up. To learn more, please visit our website</p>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare features</h4>
<ul class="menu menu-sidebar">
<li><a href="https://www.cloudflare.com/">Overview</a></li>
<li><a href="https://www.cloudflare.com/cdn/">CDN</a></li>
<li><a href="https://www.cloudflare.com/website-optimization/">Optimizer</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/analytics/">Analytics</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network/">Network map</a></li>
<li><a href="https://www.cloudflarestatus.com">System status</a></li>
</ul>
</div>
<div id="mc_embed_signup" class="widget">
<form action="https://cloudflare.us5.list-manage.com/subscribe/post?u=d80d4d74266c0c044b0bcd7ca&amp;id=8dc0bf9dea" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<input type="email" value="" name="EMAIL" class="width-full required email" id="mce-EMAIL" placeholder="Enter your email address"/>
<div id="mce-responses" class="clearfix">
<div class="response" id="mce-error-response" style="display:none"></div>
<div class="response" id="mce-success-response" style="display:none"></div>
</div>
<div class="clearfix">
<button type="submit" name="subscribe" id="mc-embedded-subscribe" class="btn btn-primary width-full">Sign up for email updates</button>
</div>
</form>
</div>
</aside>
</div>
<footer id="footer" class="footer">
<div class="wrapper">
<nav class="navigation footer-nav">
<ul role="navigation">
<li id="cf_nav_menu-2" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">What We Do</h6>
<div class="menu-what-we-do-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/plans">Plans</a></li>
<li><a href="https://www.cloudflare.com/performance/">Performance</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/reliability/">Reliability</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network-map">Network</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-3" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Resources</h6>
<div class="menu-support-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/support">Help Center</a></li>
<li><a href="https://www.cloudflare.com/community">Community</a></li>
<li><a href="https://www.cloudflare.com/video">Video Guides</a></li>
<li><a href="https://www.cloudflarestatus.com">System Status</a></li>
<li><a href="https://www.cloudflare.com/contact">Contact Us</a></li>
<li class="active"><a href="/">Blog</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-4" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Not a Developer?</h6>
<div class="menu-resources-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/case-studies">Case Studies</a></li>
<li><a href="https://www.cloudflare.com/resources/">White Papers</a></li>
<li><a href="https://www.cloudflare.com/internet-summit/">Internet Summit</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Partners</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Integrations</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-5" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">About Us</h6>
<div class="menu-about-us-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/people">Our Team</a></li>
<li><a href="https://www.cloudflare.com/join-our-team">Careers</a></li>
<li><a href="https://www.cloudflare.com/press-center">Press</a></li>
<li><a href="https://www.cloudflare.com/terms">Terms of Service</a></li>
<li><a href="https://www.cloudflare.com/security-policy/">Privacy &amp; Security</a></li>
<li><a href="https://www.cloudflare.com/abuse/">Trust &amp; Safety</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-6" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Connect</h6>
<div class="menu-connect-container">
<ul class="menu menu-footer">
<li><a href="http://twitter.com/cloudflare">Twitter</a></li>
<li><a href="https://www.facebook.com/Cloudflare">Facebook</a></li>
<li><a href="https://www.linkedin.com/company/cloudflare-inc-">LinkedIn</a></li>
<li><a href="https://www.youtube.com/cloudflare-">YouTube</a></li>
<li><a href="https://plus.google.com/+cloudflare/posts">Google+</a></li>
<li><a href="/rss/">RSS</a></li>
</ul>
</div>
</li>
</ul>
<div class="credits">All content &copy; 2017 <a href="https://cloudflare.com">Cloudflare</a>. Proudly published with <a href="https://ghost.org">Ghost</a>.</div>
</nav>
</div>
</footer>
<script>
var links = document.links;
for (var i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) {
links[i].target = '_blank';
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/prism.min.js"></script>
<script type="text/javascript" src="/assets/js/jquery.fitvids.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
$(document).ready(function(){ $(".post-content").fitVids(); });
</script>
<script type="text/javascript">
var disqus_shortname = 'cloudflare';
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
s.src = '//' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
</script>
</body>
</html>

515
h2mux/sample/index1.html Normal file
View File

@ -0,0 +1,515 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Living In A Multi-Cloud World</title>
<meta name="description" content="At our recent Internet Summit, we hosted a discussion on A Cloud Without Handcuffs with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS." />
<meta name="HandheldFriendly" content="True">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<link rel="shortcut icon" href="/assets/images/favicon.ico?v=b6cf3f99a6">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/assets/images/apple-touch-icon-57x57-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/assets/images/apple-touch-icon-72x72-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/assets/images/apple-touch-icon-114x114-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/assets/images/apple-touch-icon-144x144-precomposed.png?v=b6cf3f99a6" />
<link rel="stylesheet" type="text/css" href="/assets/css/screen.css?v=b6cf3f99a6" />
<!--[if lt IE 9]><link rel="stylesheet" type="text/css" href="/assets/css/ie.css?v=b6cf3f99a6" /><![endif]-->
<!--<link href="http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,700,300,600" rel="stylesheet" type="text/css">-->
<script>(function(G,o,O,g,l){G.GoogleAnalyticsObject=O;G[O]||(G[O]=function(){(G[O].q=G[O].q||[]).push(arguments)});G[O].l=+new Date;g=o.createElement('script'),l=o.scripts[0];g.src='//www.google-analytics.com/analytics.js';l.parentNode.insertBefore(g,l)}(this,document,'ga'));ga('create','UA-10218544-12', 'auto');ga('send','pageview')</script>
<link rel="canonical" href="http://blog.cloudflare.com/living-in-a-multi-cloud-world/" />
<meta name="referrer" content="no-referrer-when-downgrade" />
<link rel="amphtml" href="http://blog.cloudflare.com/living-in-a-multi-cloud-world/amp/" />
<meta property="og:site_name" content="Cloudflare Blog" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Living In A Multi-Cloud World" />
<meta property="og:description" content="At our recent Internet Summit, we hosted a discussion on A Cloud Without Handcuffs with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS." />
<meta property="og:url" content="http://blog.cloudflare.com/living-in-a-multi-cloud-world/" />
<meta property="og:image" content="http://blog.cloudflare.com/content/images/2017/11/Cloudflare_Multi_Cloud-1.png" />
<meta property="article:published_time" content="2017-11-21T16:30:00.000Z" />
<meta property="article:modified_time" content="2017-11-21T16:35:36.000Z" />
<meta property="article:tag" content="Google Cloud" />
<meta property="article:tag" content="Cloud Computing" />
<meta property="article:tag" content="Internet Summit" />
<meta property="article:publisher" content="https://www.facebook.com/Cloudflare" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Living In A Multi-Cloud World" />
<meta name="twitter:description" content="At our recent Internet Summit, we hosted a discussion on A Cloud Without Handcuffs with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS." />
<meta name="twitter:url" content="http://blog.cloudflare.com/living-in-a-multi-cloud-world/" />
<meta name="twitter:image" content="http://blog.cloudflare.com/content/images/2017/11/Cloudflare_Multi_Cloud-1.png" />
<meta name="twitter:label1" content="Written by" />
<meta name="twitter:data1" content="Sergi Isasi" />
<meta name="twitter:label2" content="Filed under" />
<meta name="twitter:data2" content="Google Cloud, Cloud Computing, Internet Summit" />
<meta name="twitter:site" content="@cloudflare" />
<meta name="twitter:creator" content="@sgisasi" />
<meta property="og:image:width" content="2002" />
<meta property="og:image:height" content="934" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"publisher": {
"@type": "Organization",
"name": "Cloudflare Blog",
"logo": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb.png",
"width": 189,
"height": 47
}
},
"author": {
"@type": "Person",
"name": "Sergi Isasi",
"image": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2017/11/FullSizeRender_jpeg.png",
"width": 487,
"height": 487
},
"url": "http://blog.cloudflare.com/author/sergi/",
"sameAs": [
"https://twitter.com/sgisasi"
],
"description": "Product Management @ Cloudflare. "
},
"headline": "Living In A Multi-Cloud World",
"url": "https://blog.cloudflare.com/living-in-a-multi-cloud-world/",
"datePublished": "2017-11-21T16:30:00.000Z",
"dateModified": "2017-11-21T16:35:36.000Z",
"image": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2017/11/Cloudflare_Multi_Cloud-1.png",
"width": 2002,
"height": 934
},
"keywords": "Google Cloud, Cloud Computing, Internet Summit",
"description": "At our recent Internet Summit, we hosted a discussion on A Cloud Without Handcuffs with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS.",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "http://blog.cloudflare.com"
}
}
</script>
<script type="text/javascript" src="/shared/ghost-url.min.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
ghost.init({
clientId: "ghost-frontend",
clientSecret: "cf0df60d1ab4"
});
</script>
<meta name="generator" content="Ghost 0.11" />
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
ga('send', 'event', 'recruiting', 'jobscore-click', role, {
'transport': 'beacon',
'hitCallback': function(){document.location = url;}
});
}
</script>
<script type="text/javascript">
(function() {
var didInit = false;
function initMunchkin() {
if(didInit === false) {
didInit = true;
Munchkin.init('713-XSC-918');
}
}
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = '//munchkin.marketo.net/munchkin.js';
s.onreadystatechange = function() {
if (this.readyState == 'complete' || this.readyState == 'loaded') {
initMunchkin();
}
};
s.onload = initMunchkin;
document.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script>
var HTMLAttrToAdd = document.querySelector("html");
HTMLAttrToAdd.setAttribute("lang", "en");
</script>
<style>
table {
background-color: transparent;
}
td {
padding: 5px 1em;
}
pre {
max-height: 500px;
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>
.st-default-search-input {
font-family: Helvetica, Arial, "Lucida Grande", sans-serif;
font-size: 14px;
line-height: 16px;
font-weight: 400;
-moz-transition: opacity 0.2s;
-o-transition: opacity 0.2s;
-webkit-transition: opacity 0.2s;
transition: opacity 0.2s;
display: inline-block;
width: 190px;
height: 16px;
padding: 7px 11px 7px 28px;
border: 1px solid rgba(0, 0, 0, 0.25);
color: #444;
-moz-box-sizing: content-box;
box-sizing: content-box;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #fff 8px 8px no-repeat url("data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6%2BR8AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAG11AABzoAAA%2FN0AAINkAABw6AAA7GgAADA%2BAAAQkOTsmeoAAAESSURBVHjajNCxS9VRGMbxz71E4OwgoXPQxVEpXCI47%2BZqGP0LCoJO7UVD3QZzb3SwcHB7F3Uw3Zpd%2FAPCcJKG7Dj4u%2FK7Pwp94HDg5Xyf5z1Pr9YKImKANTzFXxzjU2ae6qhXaxURr%2FAFl9hHDy%2FwEK8z89sYVEp5gh84wMvMvGiSJ%2FEV85jNzLMR1McqfmN5BEBmnmMJFSvtpH7jdJiZv7q7Z%2BZPfMdcF6rN%2FT%2F1m2LGBkd4HhFT3dcRMY2FpskxaLNpayciHrWAGeziD7b%2BVfkithuTk8bkGa4wgWFmbrSTZOYeBvjc%2BucQj%2FEe6xHx4Taq1nrnKaW8K6XUUsrHWuvNevdRRLzFGwzvDbXAB9cDAHvhedDruuxSAAAAAElFTkSuQmCC")
}
.st-ui-close-button {
-moz-transition: none;
-o-transition: none;
-webkit-transition: none;
transition: none
}
</style>
</head>
<body class="post-template tag-google-cloud tag-cloud-computing tag-internet-summit">
<div id="fb-root"></div>
<header id="header" class="header">
<div class="wrapper">
<a href="https://www.cloudflare.com" class="logo logo-header">Cloudflare</a>
<nav id="main-menu" class="header-navigation navigation" role="navigation">
<ul class="menu menu-header">
<li><a href="https://blog.cloudflare.com/">Blog home</a></li>
<li><a href="https://www.cloudflare.com/overview" tabindex="1">What we do</a></li>
<li><a href="https://www.cloudflare.com/support" tabindex="9">Support</a></li>
<li><a href="https://www.cloudflare.com/community" tabindex="9">Community</a></li>
<li><a href="https://www.cloudflare.com/login" tabindex="10">Login</a></li>
<li><a href="https://www.cloudflare.com/sign-up" class="btn btn-success" tabindex="11">Sign up</a></li>
</ul>
</nav>
</div>
</header>
<div class="wrapper reverse-sidebar">
<section class="primary-content" role="main">
<article class="post tag-google-cloud tag-cloud-computing tag-internet-summit">
<header class="post-header">
<h1 class="title">Living In A Multi-Cloud World</h1>
<div class="meta">
<time class="meta-date" datetime="2017-11-21">21 Nov 2017</time>
by <a href="/author/sergi/">Sergi Isasi</a>.
</div>
<div class="social">
<div class="g-plusone" data-size="medium" data-href="https://blog.cloudflare.com/living-in-a-multi-cloud-world/"></div>
<script type="IN/Share" data-url="https://blog.cloudflare.com/living-in-a-multi-cloud-world/" data-counter="right"></script>
<div class="fb-like" data-href="https://blog.cloudflare.com/living-in-a-multi-cloud-world/" data-layout="button_count" data-action="like" data-show-faces="false" data-share="false"></div>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="https://blog.cloudflare.com/living-in-a-multi-cloud-world/" data-text="Living In A Multi-Cloud World" data-via="cloudflare" data-related="cloudflare">Tweet</a>
</div>
</header>
<div class="post-content">
<p>A few months ago at Cloudflares <a href="https://www.cloudflare.com/internet-summit/">Internet Summit</a>, we hosted a discussion on <a href="https://blog.cloudflare.com/a-cloud-without-handcuffs/">A Cloud Without Handcuffs</a> with Joe Beda, one of the creators of Kubernetes, and Brandon Phillips, the co-founder of CoreOS. The conversation touched on multiple areas, but its clear that more and more companies are recognizing the need to have some strategy around hosting their applications on multiple cloud providers.</p>
<p>Earlier this year, Mary Meeker published her annual <a href="http://www.kpcb.com/internet-trends">Internet Trends</a> report which revealed that 22% of respondents viewed Cloud Vendor Lock-In as a top 3 concern, up from just 7% in 2012. This is in contrast to previous top concerns, Data Security and Cost &amp; Savings, both of which dropped amongst those surveyed.</p>
<p><img src="/content/images/2017/11/Mary-Meeker-Internet-Trends-2017.png" alt="Internet Trends" /></p>
<p>At Cloudflare, our mission is to help build a better internet. To fulfill this mission, our customers need to have consistent access to the best technology and services, over time. This is especially the case with respect to storage and compute providers. This means not becoming locked-in to any single provider and taking advantage of multiple cloud computing vendors (such as Amazon Web Services or Google Cloud Platform) for the same end user services. </p>
<h3 id="thebenefitsofhavingmultiplecloudvendors">The Benefits of Having Multiple Cloud Vendors</h3>
<p>There are a number of potential challenges when selecting a single cloud provider. Though there may be scenarios where it makes sense to consolidate on a single vendor, our belief is that it is important that customers are aware of their choice and downsides of being potentially locked-in to that particular vendor. In short, know what trade offs you are making should you decide to continue to consolidate parts of your network, compute, and storage with a single cloud provider. While not comprehensive, here are a few trade-offs you may be making if you are locked-in to one cloud.</p>
<h4 id="costefficiences">Cost Efficiences</h4>
<p>For some companies, there may be a cost savings involved in spreading traffic across multiple vendors. Some can take advantage of free or reduced cost tiers at lower volumes. Vendors may provide reduced costs for certain times of day that are lower utilized on their infrastructure. Applications can have varying compute requirements amongst layers of the application: some may require faster, immediate processing while others may benefit from delayed processing at a lower cost. </p>
<h4 id="negotiationstrength">Negotiation Strength</h4>
<p>One of the most important reasons to consider deploying in multiple cloud providers is to minimize your reliance on a single vendors technology for your critical business processes. As you become more vertically integrated with any vendor, your negotiation posture for pricing or favorable contract terms becomes diminished. Having production ready code available on multiple providers allows you to have less technical debt should you need to change. If you go a step further and are already sending traffic to multiple providers, you have minimized the technical debt required to switch and can negotiate from a position of strength.</p>
<h4 id="businesscontinuityorhighavailability">Business Continuity or High Availability</h4>
<p>While the major cloud providers are generally reliable, there have been a few notable outages in recent years. The most significant in recent memory being Amazons <a href="https://aws.amazon.com/message/41926/">US-EAST S3</a> outage in February. Some organizations may have a policy specifying multiple providers for high availability while others should consider it where necessary and feasible as a best practice. A multi-cloud strategy can lower operational risk from a single vendors mistakes causing a significant outage for a mission critical application.</p>
<h4 id="experimentation">Experimentation</h4>
<p>One of the exciting things about having competition in the space is the level of innovation and feature velocity of each provider. Every year there are major announcements of new products or features that may have a significant impact on improving your organization's competitive advantage. Having test and production environments in multiple providers gives your engineers the ability to understand and experiment with a new capability in the context of your technology stack and data. You may even try these features for a portion of your traffic and get real world data on any benefits realized.</p>
<h3 id="cloudflaresrole">Cloudflares Role</h3>
<p>Cloudflare is an independent third party in your multi-cloud strategy. Our goal is to minimize the layers of lock-in between you and a provider and lower the effort of change. In particular, one area where we can help right away is to minimize the operational changes necessary at the network, similar to what Kubernetes can do at the storage and compute level. As a benefit of our network, you can also have a centralized point for security and operational control.</p>
<p><img src="/content/images/2017/11/Cloudflare_Multi_Cloud.png" alt="Cloudflare Multi Cloud" /></p>
<p>Cloudflares Load Balancing can easily be configured to act as your global application traffic aggregator and distribute your traffic amongst origins at as many clouds as you choose to utilize. Active layer 7 health checks continually probe your origins and can automatically move traffic in the case of network or application failure. All consolidated web traffic can be inspected and acted upon by Cloudflares best of breed <a href="https://www.cloudflare.com/security/">Security</a> services, providing a single control point and visibility across all application traffic, regardless of which cloud the origin may be on. You also have the benefit of Cloudflares <a href="https://www.cloudflare.com/network/">Global Anycast Network</a>, providing for better speed and higher availability regardless of which clouds your origins are hosted on.</p>
<h3 id="billforwardusingcloudflaretoimplementmulticloud">Billforward: Using Cloudflare to Implement Multi-Cloud</h3>
<p>Billforward is a San Francisco and London based startup that is focused and mission driven on changing the way people bill and charge their customers, providing a solution to the complexities of Quote-to-Cash. Their platform is built on a number of Rest APIs that other developers call to bill and generate revenue for their own companies. </p>
<p>Billforward is using Cloudflare for its core customer facing application to failover traffic between Google Compute Engine and Amazon Web Services. Acting as a reverse proxy, Cloudflare receives all requests for and decides which of Billforwards two configured cloud origins to use based upon the availability of that origin in near real-time. This allows Billforward to completely manage the connections to and from two disparate cloud providers using Cloudflares UI or API. Billforward is in the process of migrating all of their customer facing domains to a similar setup.</p>
<h4 id="configuration">Configuration</h4>
<p>Billforward has a single load balanced hostname with two available Pools. Theyve named the two Pools with “gce” and “aws” labels and each Pool has one Origin associated with it. All of the Pools are enabled and the entire LB/hostname is proxied through Cloudflare (as indicated by the orange cloud).</p>
<p><img src="/content/images/2017/11/Billforward_Config_UI.png" alt="Billforward Configuration UI" /></p>
<p>Cloudflare probes Billforwards Origins once every minute from all of Cloudflares data centers around the world (a feature available to all Load Balancing Enterprise customers). If Billforwards GCE Origin goes down, Cloudflare will quickly and automatically failover to the AWS Origin with no actions required from Billforwards team.</p>
<p>Google Compute Engine was chosen as the primary provider for this application by virtue of cost. Martin Lee, Site Reliability Engineer at Billforward says, “Essentially, GCE is cheaper for our general purpose computing needs but we're more experienced with deployments in AWS. This strategy allows us to switch back and forth at will and avoid being tied in to either platform.” It is likely that Billforward will change the priority as pricing models evolve. <br />
<br> </p>
<blockquote>
<p>“It's a fairly fast moving world and features released by cloud providers can have a meaningful impact on performance and cost on a week by week basis - it helps to stay flexible,” says Martin. “We may also change priority based on features.”</p>
</blockquote>
<p><br>For orchestration of the compute and storage layers, Billforward uses <a href="https://www.docker.com/">Docker</a> containers managed through <a href="http://www.rancher.com/">Rancher</a>. They use distinct environments between cloud providers but are considering bridging an environment across cloud providers and using VPNs between them, which will enable them to move load between providers even more easily. “Our system is loosely coupled through a message queue,” adds Martin. “Having a container system across clouds means we can really take advantage of this - we can very easily move workloads across clouds without any danger of dropping tasks or ending up in an inconsistent state.”</p>
<h4 id="benefits">Benefits</h4>
<p>Billforward manages these connections at Cloudflares edge. Through this interface (or via the Cloudflare APIs), they can also manually move traffic from GCE to AWS by just disabling the GCE pool or by rearranging the Pool priority and make AWS the primary. These changes are near instant on the Cloudflare network and require no downtime to Billforwards customer facing application. This allows them to act on potential advantageous pricing changes between the two cloud providers or move traffic to hit pricing tiers. </p>
<p>In addition, Billforward is now not “locked-in” to either providers network; being able to move traffic and without any downtime means they can make traffic changes independent of Amazon or Google. They can also integrate additional cloud providers any time they deem fit: adding Microsoft Azure, for example, as a third Origin would be as simple as creating a new Pool and adding it to the Load Balancer. </p>
<p>Billforward is a good example of a forward thinking company that is taking advantage of technologies from multiple providers to best serve their business and customers, while not being reliant on a single vendor. For further detail on their setup using Cloudflare, please check their <a href="https://www.billforward.net/blog/being-multi-cloud-with-cloudflare/">blog</a>.</p>
</div>
<footer>
<small>
Tagged with <a href="/tag/google-cloud/">Google Cloud</a>, <a href="/tag/cloud-computing/">Cloud Computing</a>, <a href="/tag/internet-summit/">Internet Summit</a>
</small>
</footer>
<aside class="section learn-more">
<h5>Want to learn more about Cloudflare?</h5>
<p><a href="https://www.cloudflare.com" class="btn btn-success">Learn more</a></p>
</aside>
<aside class="section comments">
<h3>Comments</h3>
</aside>
<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = 'cloudflare';
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
</article>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
</script>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=596756540369391";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<script src="//platform.linkedin.com/in.js" type="text/javascript">lang: en_US</script>
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/platform.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
</section>
<aside class="sidebar">
<div class="widget">
<input type="text" placeholder="Search the blog" class="st-default-search-input"></input>
<script type="text/javascript">
(function(w,d,t,u,n,s,e){w['SwiftypeObject']=n;w[n]=w[n]||function(){
(w[n].q=w[n].q||[]).push(arguments);};s=d.createElement(t);
e=d.getElementsByTagName(t)[0];s.async=0;s.src=u;e.parentNode.insertBefore(s,e);
})(window,document,'script','//s.swiftypecdn.com/install/v2/st.js','_st');
_st('install','_KobMC_zsd_tDx_7NWiX','2.0.0');
</script>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare blog</h4>
<p style="margin-top: 20px">
<a href="https://www.cloudflare.com/enterprise-service-request" class="btn btn-success" tabindex="11" target="_blank">Contact our team</a>
</p>
<p>
<strong>US callers</strong><br/>
1 (888) 99-FLARE <br/>
<strong>UK callers</strong><br/>
+44 (0)20 3514 6970<br/>
<strong>International callers</strong><br/>
+1 (650) 319-8930 <BR/><BR/>
<a href="https://www.cloudflare.com/plans" target="_blank">Full feature list and plan types</a>
</p>
<p>Cloudflare provides performance and security for any website. More than 6 million websites use Cloudflare.</p>
<p>There is no hardware or software. Cloudflare works at the DNS level. It takes only 5 minutes to sign up. To learn more, please visit our website</p>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare features</h4>
<ul class="menu menu-sidebar">
<li><a href="https://www.cloudflare.com/">Overview</a></li>
<li><a href="https://www.cloudflare.com/cdn/">CDN</a></li>
<li><a href="https://www.cloudflare.com/website-optimization/">Optimizer</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/analytics/">Analytics</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network/">Network map</a></li>
<li><a href="https://www.cloudflarestatus.com">System status</a></li>
</ul>
</div>
<div id="mc_embed_signup" class="widget">
<form action="https://cloudflare.us5.list-manage.com/subscribe/post?u=d80d4d74266c0c044b0bcd7ca&amp;id=8dc0bf9dea" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<input type="email" value="" name="EMAIL" class="width-full required email" id="mce-EMAIL" placeholder="Enter your email address"/>
<div id="mce-responses" class="clearfix">
<div class="response" id="mce-error-response" style="display:none"></div>
<div class="response" id="mce-success-response" style="display:none"></div>
</div>
<div class="clearfix">
<button type="submit" name="subscribe" id="mc-embedded-subscribe" class="btn btn-primary width-full">Sign up for email updates</button>
</div>
</form>
</div>
</aside>
</div>
<footer id="footer" class="footer">
<div class="wrapper">
<nav class="navigation footer-nav">
<ul role="navigation">
<li id="cf_nav_menu-2" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">What We Do</h6>
<div class="menu-what-we-do-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/plans">Plans</a></li>
<li><a href="https://www.cloudflare.com/performance/">Performance</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/reliability/">Reliability</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network-map">Network</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-3" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Resources</h6>
<div class="menu-support-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/support">Help Center</a></li>
<li><a href="https://www.cloudflare.com/community">Community</a></li>
<li><a href="https://www.cloudflare.com/video">Video Guides</a></li>
<li><a href="https://www.cloudflarestatus.com">System Status</a></li>
<li><a href="https://www.cloudflare.com/contact">Contact Us</a></li>
<li class="active"><a href="/">Blog</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-4" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Not a Developer?</h6>
<div class="menu-resources-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/case-studies">Case Studies</a></li>
<li><a href="https://www.cloudflare.com/resources/">White Papers</a></li>
<li><a href="https://www.cloudflare.com/internet-summit/">Internet Summit</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Partners</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Integrations</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-5" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">About Us</h6>
<div class="menu-about-us-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/people">Our Team</a></li>
<li><a href="https://www.cloudflare.com/join-our-team">Careers</a></li>
<li><a href="https://www.cloudflare.com/press-center">Press</a></li>
<li><a href="https://www.cloudflare.com/terms">Terms of Service</a></li>
<li><a href="https://www.cloudflare.com/security-policy/">Privacy &amp; Security</a></li>
<li><a href="https://www.cloudflare.com/abuse/">Trust &amp; Safety</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-6" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Connect</h6>
<div class="menu-connect-container">
<ul class="menu menu-footer">
<li><a href="http://twitter.com/cloudflare">Twitter</a></li>
<li><a href="https://www.facebook.com/Cloudflare">Facebook</a></li>
<li><a href="https://www.linkedin.com/company/cloudflare-inc-">LinkedIn</a></li>
<li><a href="https://www.youtube.com/cloudflare-">YouTube</a></li>
<li><a href="https://plus.google.com/+cloudflare/posts">Google+</a></li>
<li><a href="/rss/">RSS</a></li>
</ul>
</div>
</li>
</ul>
<div class="credits">All content &copy; 2017 <a href="https://cloudflare.com">Cloudflare</a>. Proudly published with <a href="https://ghost.org">Ghost</a>.</div>
</nav>
</div>
</footer>
<script>
var links = document.links;
for (var i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) {
links[i].target = '_blank';
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/prism.min.js"></script>
<script type="text/javascript" src="/assets/js/jquery.fitvids.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
$(document).ready(function(){ $(".post-content").fitVids(); });
</script>
<script type="text/javascript">
var disqus_shortname = 'cloudflare';
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
s.src = '//' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
</script>
</body>
</html>

502
h2mux/sample/index2.html Normal file
View File

@ -0,0 +1,502 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>SCOTUS Wanders into Patent Troll Fight</title>
<meta name="description" content="" />
<meta name="HandheldFriendly" content="True">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<link rel="shortcut icon" href="/assets/images/favicon.ico?v=b6cf3f99a6">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/assets/images/apple-touch-icon-57x57-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/assets/images/apple-touch-icon-72x72-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/assets/images/apple-touch-icon-114x114-precomposed.png?v=b6cf3f99a6" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/assets/images/apple-touch-icon-144x144-precomposed.png?v=b6cf3f99a6" />
<link rel="stylesheet" type="text/css" href="/assets/css/screen.css?v=b6cf3f99a6" />
<!--[if lt IE 9]><link rel="stylesheet" type="text/css" href="/assets/css/ie.css?v=b6cf3f99a6" /><![endif]-->
<!--<link href="http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,700,300,600" rel="stylesheet" type="text/css">-->
<script>(function(G,o,O,g,l){G.GoogleAnalyticsObject=O;G[O]||(G[O]=function(){(G[O].q=G[O].q||[]).push(arguments)});G[O].l=+new Date;g=o.createElement('script'),l=o.scripts[0];g.src='//www.google-analytics.com/analytics.js';l.parentNode.insertBefore(g,l)}(this,document,'ga'));ga('create','UA-10218544-12', 'auto');ga('send','pageview')</script>
<link rel="canonical" href="http://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" />
<meta name="referrer" content="no-referrer-when-downgrade" />
<link rel="amphtml" href="http://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/amp/" />
<meta property="og:site_name" content="Cloudflare Blog" />
<meta property="og:type" content="article" />
<meta property="og:title" content="SCOTUS Wanders into Patent Troll Fight" />
<meta property="og:description" content="Next Monday, the US Supreme Court will hear oral arguments in Oil States Energy Services, LLC vs. Greenes Energy Group, LLC, which is a case to determine whether the Inter Partes Review (IPR) administrative process at the US Patent and Trademark Office (USPTO) used to determine the validity of" />
<meta property="og:url" content="http://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" />
<meta property="og:image" content="http://blog.cloudflare.com/content/images/2017/11/Thomas_Rowlandson_-_The_Privy_Council_of_a_King_-_Google_Art_Project--1-.jpg" />
<meta property="article:published_time" content="2017-11-20T18:18:00.000Z" />
<meta property="article:modified_time" content="2017-11-20T22:51:13.000Z" />
<meta property="article:tag" content="Legal" />
<meta property="article:tag" content="Jengo" />
<meta property="article:tag" content="Patents" />
<meta property="article:publisher" content="https://www.facebook.com/Cloudflare" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="SCOTUS Wanders into Patent Troll Fight" />
<meta name="twitter:description" content="Next Monday, the US Supreme Court will hear oral arguments in Oil States Energy Services, LLC vs. Greenes Energy Group, LLC, which is a case to determine whether the Inter Partes Review (IPR) administrative process at the US Patent and Trademark Office (USPTO) used to determine the validity of" />
<meta name="twitter:url" content="http://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" />
<meta name="twitter:image" content="http://blog.cloudflare.com/content/images/2017/11/Thomas_Rowlandson_-_The_Privy_Council_of_a_King_-_Google_Art_Project--1-.jpg" />
<meta name="twitter:label1" content="Written by" />
<meta name="twitter:data1" content="Edo Royker" />
<meta name="twitter:label2" content="Filed under" />
<meta name="twitter:data2" content="Legal, Jengo, Patents" />
<meta name="twitter:site" content="@cloudflare" />
<meta property="og:image:width" content="4468" />
<meta property="og:image:height" content="3183" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"publisher": {
"@type": "Organization",
"name": "Cloudflare Blog",
"logo": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2016/09/logo-for-blog_thumb.png",
"width": 189,
"height": 47
}
},
"author": {
"@type": "Person",
"name": "Edo Royker",
"image": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2017/11/AAEAAQAAAAAAAAdiAAAAJDdiMzU0OWYxLTBmOTMtNGZhZi1hNDQ1LTBhNjJhZDdmMGRlZA.jpg",
"width": 200,
"height": 200
},
"url": "http://blog.cloudflare.com/author/edo-royker/",
"sameAs": []
},
"headline": "SCOTUS Wanders into Patent Troll Fight",
"url": "https://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/",
"datePublished": "2017-11-20T18:18:00.000Z",
"dateModified": "2017-11-20T22:51:13.000Z",
"image": {
"@type": "ImageObject",
"url": "http://blog.cloudflare.com/content/images/2017/11/Thomas_Rowlandson_-_The_Privy_Council_of_a_King_-_Google_Art_Project--1-.jpg",
"width": 4468,
"height": 3183
},
"keywords": "Legal, Jengo, Patents",
"description": "Next Monday, the US Supreme Court will hear oral arguments in Oil States Energy Services, LLC vs. Greenes Energy Group, LLC, which is a case to determine whether the Inter Partes Review (IPR) administrative process at the US Patent and Trademark Office (USPTO) used to determine the validity of",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "http://blog.cloudflare.com"
}
}
</script>
<script type="text/javascript" src="/shared/ghost-url.min.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
ghost.init({
clientId: "ghost-frontend",
clientSecret: "cf0df60d1ab4"
});
</script>
<meta name="generator" content="Ghost 0.11" />
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
ga('send', 'event', 'recruiting', 'jobscore-click', role, {
'transport': 'beacon',
'hitCallback': function(){document.location = url;}
});
}
</script>
<script type="text/javascript">
(function() {
var didInit = false;
function initMunchkin() {
if(didInit === false) {
didInit = true;
Munchkin.init('713-XSC-918');
}
}
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = '//munchkin.marketo.net/munchkin.js';
s.onreadystatechange = function() {
if (this.readyState == 'complete' || this.readyState == 'loaded') {
initMunchkin();
}
};
s.onload = initMunchkin;
document.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script>
var HTMLAttrToAdd = document.querySelector("html");
HTMLAttrToAdd.setAttribute("lang", "en");
</script>
<style>
table {
background-color: transparent;
}
td {
padding: 5px 1em;
}
pre {
max-height: 500px;
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>
.st-default-search-input {
font-family: Helvetica, Arial, "Lucida Grande", sans-serif;
font-size: 14px;
line-height: 16px;
font-weight: 400;
-moz-transition: opacity 0.2s;
-o-transition: opacity 0.2s;
-webkit-transition: opacity 0.2s;
transition: opacity 0.2s;
display: inline-block;
width: 190px;
height: 16px;
padding: 7px 11px 7px 28px;
border: 1px solid rgba(0, 0, 0, 0.25);
color: #444;
-moz-box-sizing: content-box;
box-sizing: content-box;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #fff 8px 8px no-repeat url("data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6%2BR8AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAG11AABzoAAA%2FN0AAINkAABw6AAA7GgAADA%2BAAAQkOTsmeoAAAESSURBVHjajNCxS9VRGMbxz71E4OwgoXPQxVEpXCI47%2BZqGP0LCoJO7UVD3QZzb3SwcHB7F3Uw3Zpd%2FAPCcJKG7Dj4u%2FK7Pwp94HDg5Xyf5z1Pr9YKImKANTzFXxzjU2ae6qhXaxURr%2FAFl9hHDy%2FwEK8z89sYVEp5gh84wMvMvGiSJ%2FEV85jNzLMR1McqfmN5BEBmnmMJFSvtpH7jdJiZv7q7Z%2BZPfMdcF6rN%2FT%2F1m2LGBkd4HhFT3dcRMY2FpskxaLNpayciHrWAGeziD7b%2BVfkithuTk8bkGa4wgWFmbrSTZOYeBvjc%2BucQj%2FEe6xHx4Taq1nrnKaW8K6XUUsrHWuvNevdRRLzFGwzvDbXAB9cDAHvhedDruuxSAAAAAElFTkSuQmCC")
}
.st-ui-close-button {
-moz-transition: none;
-o-transition: none;
-webkit-transition: none;
transition: none
}
</style>
</head>
<body class="post-template tag-legal tag-jengo tag-patents">
<div id="fb-root"></div>
<header id="header" class="header">
<div class="wrapper">
<a href="https://www.cloudflare.com" class="logo logo-header">Cloudflare</a>
<nav id="main-menu" class="header-navigation navigation" role="navigation">
<ul class="menu menu-header">
<li><a href="https://blog.cloudflare.com/">Blog home</a></li>
<li><a href="https://www.cloudflare.com/overview" tabindex="1">What we do</a></li>
<li><a href="https://www.cloudflare.com/support" tabindex="9">Support</a></li>
<li><a href="https://www.cloudflare.com/community" tabindex="9">Community</a></li>
<li><a href="https://www.cloudflare.com/login" tabindex="10">Login</a></li>
<li><a href="https://www.cloudflare.com/sign-up" class="btn btn-success" tabindex="11">Sign up</a></li>
</ul>
</nav>
</div>
</header>
<div class="wrapper reverse-sidebar">
<section class="primary-content" role="main">
<article class="post tag-legal tag-jengo tag-patents">
<header class="post-header">
<h1 class="title">The Supreme Court Wanders into the Patent Troll Fight</h1>
<div class="meta">
<time class="meta-date" datetime="2017-11-20">20 Nov 2017</time>
by <a href="/author/edo-royker/">Edo Royker</a>.
</div>
<div class="social">
<div class="g-plusone" data-size="medium" data-href="https://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/"></div>
<script type="IN/Share" data-url="https://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" data-counter="right"></script>
<div class="fb-like" data-href="https://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" data-layout="button_count" data-action="like" data-show-faces="false" data-share="false"></div>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="https://blog.cloudflare.com/supreme-court-wanders-into-patent-troll-fight/" data-text="The Supreme Court Wanders into the Patent Troll Fight" data-via="cloudflare" data-related="cloudflare">Tweet</a>
</div>
</header>
<div class="post-content">
<p>Next Monday, the US Supreme Court will hear oral arguments in <em>Oil States Energy Services, LLC vs. Greenes Energy Group, LLC</em>, which is a case to determine whether the Inter Partes Review (IPR) administrative process at the US Patent and Trademark Office (USPTO) used to determine the validity of patents is constitutional. </p>
<p>The constitutionality of the IPR process is one of the biggest legal issues facing innovative technology companies, as the availability of this process has greatly reduced the anticipated costs, and thereby lessened the threat, of patent troll litigation. As we discuss in this blog post, it is ironic that the outcome of a case that is of such great importance to the technology community today may hinge on what courts in Britain were and were not doing more than 200 years ago.</p>
<p><img src="/content/images/2017/11/Thomas_Rowlandson_-_The_Privy_Council_of_a_King_-_Google_Art_Project.jpg" alt="" title="" /><small>Thomas Rowlandson [Public domain], via <a href="https://commons.wikimedia.org/wiki/File%3AThomas_Rowlandson_-_The_Privy_Council_of_a_King_-_Google_Art_Project.jpg">Wikimedia Commons</a></small></p>
<p>As we have discussed in prior <a href="https://blog.cloudflare.com/project-jengo-challenges/">blog posts</a>, the stakes are high: if the Supreme Court finds IPR unconstitutional, then the entire system of administrative review by the USPTO — including IPR and ex parte processes — will be shuttered. This would be a mistake, as administrative recourse at the USPTO is one of the few ways to avoid the considerable costs and delays of federal court litigation, which can take years and run into the millions of dollars. Those heavy costs are often leveraged by patent trolls when they threaten litigation in the effort to procure easy and lucrative settlements from their targets. </p>
<h3 id="cloudflareispursuingourfightagainstpatenttrollsallthewaytothestepsofthesupremecourt">Cloudflare is Pursuing Our Fight Against Patent Trolls All the Way to the Steps of the Supreme Court</h3>
<p>Cloudflare joined Dell, Facebook, and a number of other companies, all practicing entities with large patent portfolios, in a <em>brief amici curiae</em> (or friend of the court brief) in support of the IPR process, because it has a substantial positive impact on technological innovation in the United States. Amicus briefs allow parties who are interested in the outcome of a case, but are not parties to the immediate dispute before the court, to have input into the courts deliberations. </p>
<p>As many of you are aware, we were sued by Blackbird Technologies, a notorious patent troll, earlier this year for patent infringement, and initiated <a href="https://blog.cloudflare.com/project-jengo/">Project Jengo</a> to crowd source prior art searches and invalidate Blackbirds patents. One of our strategies for quickly and efficiently invalidating Blackbirds patents is to take advantage of the IPR process at the USPTO, which can be completed in about half the time and at one tenth of the cost of a federal court case, and to initiate ex parte proceedings against Blackbirds other patents that are overly broad and invalid. </p>
<p>A full copy of the Amicus Brief we joined in the Oil States case is <a href="http://www.scotusblog.com/wp-content/uploads/2017/11/16-712-bsac-Dell.pdf">available here</a>, and a summary of the argument follows. </p>
<h3 id="oilstatesmakesitscase">Oil States Makes its Case</h3>
<p>Oil States is an oilfield services and drilling equipment manufacturing company. The USPTO invalidated one of its patents related to oil drilling technology in an IPR proceeding while Oil States had a lawsuit pending against one of its competitors claiming infringement of its patent. After it lost the IPR, Oil States lost an appeal in a lower federal court based on the findings of the IPR proceeding. The Supreme Court agreed to hear the case to determine whether once the USPTO issues a patent, an inventor has a constitutionally protected property right that — under <a href="http://www.heritage.org/constitution/#!/articles/3">Article III</a> of the U.S. Constitution (which outlines the powers of the judicial branch of the government), and the <a href="https://constitutioncenter.org/interactive-constitution/amendments/amendment-vii">7th Amendment</a> (which addresses the right to a jury trial in certain types of cases) — cannot be revoked without intervention by the court system. </p>
<p><img src="/content/images/2017/11/2770193028_68edc662a9_b.jpg" alt="" title="" /><small><a href="https://www.flickr.com/photos/paul_lowry/2770193028">Image</a> by <a href="https://creativecommons.org/licenses/by/2.0/">Paul Lowry</a></small></p>
<p>As the patent owner, Oil States argues that the IPR process violates the relevant provisions of the constitution by allowing an administrative body, the Patent Trial and Appeal Board (PTAB)--a non-judicial forum, to decide a matter which was historically handled by the judiciary. This argument rests upon the premise that there was a historical analogue to cancellation of patent claims available in the judiciary. Since cancellation of patent claims was historically available in the judiciary, the cancellation of patent claims today must be consistent with that history and done exclusively by courts. </p>
<p>This argument is flawed on multiple counts, which are set forth in the “friend of the court” brief we joined.</p>
<h4 id="firstflawanadministrativeprocessevenanoriginalistcanlove">First Flaw: An Administrative Process Even an Originalist Can Love</h4>
<p>As the amicus brief we joined points out, patent revocation did not historically rest within the <em>exclusive</em> province of the common law and chancery courts, the historical equivalents in Britain to the judiciary in the United States. Rather, prior to the Founding of the United States, patent revocation rested entirely with the Crown of Englands Privy Council, a non-judicial body comprising of advisors to the king or queen of England. It wasnt until later that the Privy Council granted the chancery court (the judiciary branch) concurrent authority to revoke patents. Because a non-judicial body had the authority to revoke patents when the US Constitution was framed, the general principles of separation of powers and the right to trial in the Constitution do not require that patentability challenges be decided solely by courts. </p>
<h4 id="secondflawthejudicialrolewaslimited">Second Flaw: The Judicial Role was Limited</h4>
<p>Not only did British courts share the power to address patent rights historically, the part shared by the the courts was significantly limited. Historically, the common-law and chancery courts only received a partial delegation of the Privy Councils authority to invalidate patents. Courts only had the authority to invalidate patents for issues related to things like inequitable conduct (e.g., making false statements in the original patent application). The limited authority delegated to the England Courts did not include the authority to seek claim <em>cancellation</em> based on elements intrinsic to the patent or patent application, like lack of novelty or obviousness as done under an IPR proceeding. Rather, such authority remained with the Privy Council, a non-court authority, which decided questions like whether the invention was really new. Thus, like the PTAB, the Privy Council was a non-judicial body charged with responsibility to assess patent validity based on criteria that included the novelty of the invention.</p>
<p>We think these arguments are compelling and provide very strong reasons why the Supreme Court should resist the request that such matters be resolved exclusively in federal courts. We hope thats the position they do take because the real world implications are significant. </p>
<h3 id="dontmesswithagoodthing">Dont Mess with a Good Thing</h3>
<p>The IPR process is not only consistent with the US Constitution, but it also advances the Patent Clauses objective of promoting the progress of science and useful arts. That is, the “quid pro quo of the patent system; the public must receive meaningful disclosure in exchange for being excluded from practicing the invention for a limited period of time” by patent rights. (<a href="http://caselaw.findlaw.com/us-federal-circuit/1330083.html">Enzo Biochem, Inc. v. Gen-probe Inc.</a>) Congress created the IPR process in the America Invents Act in 2011 to use administrative review to weed out poor-quality patents that did not satisfy this quid pro quo because they had not actually disclosed very much. Congress sought to provide quick and cost effective administrative procedures for challenging the validity of patent claims that did not disclose novel inventions, or that claimed to disclose substantially more innovation than they actually did, to improve patent quality and restore confidence in the presumption of validity. In other words, Congress created a system to specifically permit the efficient challenge of the zealous assertion of vague and overly broad patents. </p>
<p>As a recent study by the Congressional Research Service found, non-practicing entity (i.e., patent troll) patent litigation “activity cost defendants and licensees $29 billion in 2011, a 400 percent increase over $7 billion in 2005” and “the losses are mostly deadweight, with less than 25 percent flowing to innovation and at least that much going towards legal fees.” (<em>see</em> <a href="https://fas.org/sgp/crs/misc/R42668.pdf">Brian T. Yeh, Cong. Research sERV., R42668</a>) The IPR process enables innovative companies to navigate patent troll activity in an efficient manner and devote a greater proportion of their resources to research and development, rather than litigation or cost-of-litigation settlement fees for invalid patents. </p>
<p><img src="/content/images/2017/11/Troll-slip.jpg" alt="" title="" /><small>By EFF-Graphics (<a href="http://creativecommons.org/licenses/by/3.0/us/deed.en">Own work</a>), via <a href="https://commons.wikimedia.org/wiki/File%3ATroll-slip.jpg">Wikimedia Commons</a></small></p>
<p>Additionally, the IPR process reduces the total number and associated costs of patent disputes in a number of ways.</p>
<ul>
<li><p>Patent owners, especially patent trolls, are less likely to threaten litigation or file an infringement suit based on patent claims that they know or suspect to be invalid. In fact, patent owners who threaten or file suit merely to seek cost-of-litigation settlements have become far less prevalent because of the availability of the IPR process to reduce the cost of litigation.</p></li>
<li><p>Patent owners are less likely to initiate litigation out of concerns that the IPR proceedings may culminate in PTABs cancellation of all patent claims asserted in the infringement suit.</p></li>
<li><p>Where the PTAB does not cancel all asserted claims, statutory estoppel and the PTABs claim construction may serve to narrow the infringement issues to be resolved by the district court.</p></li>
</ul>
<p>Our hope is that the US Supreme Court justices take into full consideration the larger community of innovative companies that are helped by the IPR system in battling patent trolls, and do not limit their consideration to the implications on the parties to <em>Oil States</em> (neither of which is a non-practicing entity). As we have explained, not only does the IPR process enable innovative companies to focus their resources on technological innovation, instead of legal fees, but allowing the USPTO to administer IPR and ex parte proceedings is entirely consistent with the US Constitution.</p>
<p>While we await a decision in <em>Oil States</em>, expect to see Cloudflare initiate IPR and ex parte proceedings against Blackbird Technologies patents in the coming months. </p>
<p>We will make sure to keep you updated. </p>
</div>
<footer>
<small>
Tagged with <a href="/tag/legal/">Legal</a>, <a href="/tag/jengo/">Jengo</a>, <a href="/tag/patents/">Patents</a>
</small>
</footer>
<aside class="section learn-more">
<h5>Want to learn more about Cloudflare?</h5>
<p><a href="https://www.cloudflare.com" class="btn btn-success">Learn more</a></p>
</aside>
<aside class="section comments">
<h3>Comments</h3>
</aside>
<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = 'cloudflare';
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
</article>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
</script>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=596756540369391";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<script src="//platform.linkedin.com/in.js" type="text/javascript">lang: en_US</script>
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/platform.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
</section>
<aside class="sidebar">
<div class="widget">
<input type="text" placeholder="Search the blog" class="st-default-search-input"></input>
<script type="text/javascript">
(function(w,d,t,u,n,s,e){w['SwiftypeObject']=n;w[n]=w[n]||function(){
(w[n].q=w[n].q||[]).push(arguments);};s=d.createElement(t);
e=d.getElementsByTagName(t)[0];s.async=0;s.src=u;e.parentNode.insertBefore(s,e);
})(window,document,'script','//s.swiftypecdn.com/install/v2/st.js','_st');
_st('install','_KobMC_zsd_tDx_7NWiX','2.0.0');
</script>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare blog</h4>
<p style="margin-top: 20px">
<a href="https://www.cloudflare.com/enterprise-service-request" class="btn btn-success" tabindex="11" target="_blank">Contact our team</a>
</p>
<p>
<strong>US callers</strong><br/>
1 (888) 99-FLARE <br/>
<strong>UK callers</strong><br/>
+44 (0)20 3514 6970<br/>
<strong>International callers</strong><br/>
+1 (650) 319-8930 <BR/><BR/>
<a href="https://www.cloudflare.com/plans" target="_blank">Full feature list and plan types</a>
</p>
<p>Cloudflare provides performance and security for any website. More than 6 million websites use Cloudflare.</p>
<p>There is no hardware or software. Cloudflare works at the DNS level. It takes only 5 minutes to sign up. To learn more, please visit our website</p>
</div>
<div class="widget">
<h4 class="widget-title">Cloudflare features</h4>
<ul class="menu menu-sidebar">
<li><a href="https://www.cloudflare.com/">Overview</a></li>
<li><a href="https://www.cloudflare.com/cdn/">CDN</a></li>
<li><a href="https://www.cloudflare.com/website-optimization/">Optimizer</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/analytics/">Analytics</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network/">Network map</a></li>
<li><a href="https://www.cloudflarestatus.com">System status</a></li>
</ul>
</div>
<div id="mc_embed_signup" class="widget">
<form action="https://cloudflare.us5.list-manage.com/subscribe/post?u=d80d4d74266c0c044b0bcd7ca&amp;id=8dc0bf9dea" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<input type="email" value="" name="EMAIL" class="width-full required email" id="mce-EMAIL" placeholder="Enter your email address"/>
<div id="mce-responses" class="clearfix">
<div class="response" id="mce-error-response" style="display:none"></div>
<div class="response" id="mce-success-response" style="display:none"></div>
</div>
<div class="clearfix">
<button type="submit" name="subscribe" id="mc-embedded-subscribe" class="btn btn-primary width-full">Sign up for email updates</button>
</div>
</form>
</div>
</aside>
</div>
<footer id="footer" class="footer">
<div class="wrapper">
<nav class="navigation footer-nav">
<ul role="navigation">
<li id="cf_nav_menu-2" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">What We Do</h6>
<div class="menu-what-we-do-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/plans">Plans</a></li>
<li><a href="https://www.cloudflare.com/performance/">Performance</a></li>
<li><a href="https://www.cloudflare.com/security/">Security</a></li>
<li><a href="https://www.cloudflare.com/reliability/">Reliability</a></li>
<li><a href="https://www.cloudflare.com/apps">Apps</a></li>
<li><a href="https://www.cloudflare.com/network-map">Network</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-3" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Resources</h6>
<div class="menu-support-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/support">Help Center</a></li>
<li><a href="https://www.cloudflare.com/community">Community</a></li>
<li><a href="https://www.cloudflare.com/video">Video Guides</a></li>
<li><a href="https://www.cloudflarestatus.com">System Status</a></li>
<li><a href="https://www.cloudflare.com/contact">Contact Us</a></li>
<li class="active"><a href="/">Blog</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-4" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Not a Developer?</h6>
<div class="menu-resources-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/case-studies">Case Studies</a></li>
<li><a href="https://www.cloudflare.com/resources/">White Papers</a></li>
<li><a href="https://www.cloudflare.com/internet-summit/">Internet Summit</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Partners</a></li>
<li><a href="https://www.cloudflare.com/hosting-partners">Integrations</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-5" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">About Us</h6>
<div class="menu-about-us-container">
<ul class="menu menu-footer">
<li><a href="https://www.cloudflare.com/people">Our Team</a></li>
<li><a href="https://www.cloudflare.com/join-our-team">Careers</a></li>
<li><a href="https://www.cloudflare.com/press-center">Press</a></li>
<li><a href="https://www.cloudflare.com/terms">Terms of Service</a></li>
<li><a href="https://www.cloudflare.com/security-policy/">Privacy &amp; Security</a></li>
<li><a href="https://www.cloudflare.com/abuse/">Trust &amp; Safety</a></li>
</ul>
</div>
</li>
<li id="cf_nav_menu-6" class="footer-column widget_cf_nav_menu">
<h6 class="widget-title">Connect</h6>
<div class="menu-connect-container">
<ul class="menu menu-footer">
<li><a href="http://twitter.com/cloudflare">Twitter</a></li>
<li><a href="https://www.facebook.com/Cloudflare">Facebook</a></li>
<li><a href="https://www.linkedin.com/company/cloudflare-inc-">LinkedIn</a></li>
<li><a href="https://www.youtube.com/cloudflare-">YouTube</a></li>
<li><a href="https://plus.google.com/+cloudflare/posts">Google+</a></li>
<li><a href="/rss/">RSS</a></li>
</ul>
</div>
</li>
</ul>
<div class="credits">All content &copy; 2017 <a href="https://cloudflare.com">Cloudflare</a>. Proudly published with <a href="https://ghost.org">Ghost</a>.</div>
</nav>
</div>
</footer>
<script>
var links = document.links;
for (var i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) {
links[i].target = '_blank';
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/prism.min.js"></script>
<script type="text/javascript" src="/assets/js/jquery.fitvids.js?v=b6cf3f99a6"></script>
<script type="text/javascript">
$(document).ready(function(){ $(".post-content").fitVids(); });
</script>
<script type="text/javascript">
var disqus_shortname = 'cloudflare';
(function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
s.src = '//' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
}());
</script>
</body>
</html>

View File

@ -0,0 +1,74 @@
/*global jQuery */
/*jshint multistr:true browser:true */
/*!
* FitVids 1.0.3
*
* Copyright 2013, Chris Coyier - http://css-tricks.com + Dave Rupert - http://daverupert.com
* Credit to Thierry Koblentz - http://www.alistapart.com/articles/creating-intrinsic-ratios-for-video/
* Released under the WTFPL license - http://sam.zoy.org/wtfpl/
*
* Date: Thu Sept 01 18:00:00 2011 -0500
*/
(function( $ ){
"use strict";
$.fn.fitVids = function( options ) {
var settings = {
customSelector: null
};
if(!document.getElementById('fit-vids-style')) {
var div = document.createElement('div'),
ref = document.getElementsByTagName('base')[0] || document.getElementsByTagName('script')[0],
cssStyles = '&shy;<style>.fluid-width-video-wrapper{width:100%;position:relative;padding:0;}.fluid-width-video-wrapper iframe,.fluid-width-video-wrapper object,.fluid-width-video-wrapper embed {position:absolute;top:0;left:0;width:100%;height:100%;}</style>';
div.className = 'fit-vids-style';
div.id = 'fit-vids-style';
div.style.display = 'none';
div.innerHTML = cssStyles;
ref.parentNode.insertBefore(div,ref);
}
if ( options ) {
$.extend( settings, options );
}
return this.each(function(){
var selectors = [
"iframe[src*='player.vimeo.com']",
"iframe[src*='youtube.com']",
"iframe[src*='youtube-nocookie.com']",
"iframe[src*='kickstarter.com'][src*='video.html']",
"object",
"embed"
];
if (settings.customSelector) {
selectors.push(settings.customSelector);
}
var $allVideos = $(this).find(selectors.join(','));
$allVideos = $allVideos.not("object object"); // SwfObj conflict patch
$allVideos.each(function(){
var $this = $(this);
if (this.tagName.toLowerCase() === 'embed' && $this.parent('object').length || $this.parent('.fluid-width-video-wrapper').length) { return; }
var height = ( this.tagName.toLowerCase() === 'object' || ($this.attr('height') && !isNaN(parseInt($this.attr('height'), 10))) ) ? parseInt($this.attr('height'), 10) : $this.height(),
width = !isNaN(parseInt($this.attr('width'), 10)) ? parseInt($this.attr('width'), 10) : $this.width(),
aspectRatio = height / width;
if(!$this.attr('id')){
var videoID = 'fitvid' + Math.floor(Math.random()*999999);
$this.attr('id', videoID);
}
$this.wrap('<div class="fluid-width-video-wrapper"></div>').parent('.fluid-width-video-wrapper').css('padding-top', (aspectRatio * 100)+"%");
$this.removeAttr('height').removeAttr('width');
});
});
};
// Works with either jQuery or Zepto
})( window.jQuery || window.Zepto );

70
h2mux/sample/screen.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ package log
import (
"encoding/json"
"fmt"
"runtime"
"time"
"github.com/mattn/go-colorable"
@ -23,8 +24,7 @@ type JSONFormatter struct {
func CreateLogger() *logrus.Logger {
logger := logrus.New()
logger.Out = colorable.NewColorableStderr()
logger.Formatter = &logrus.TextFormatter{ForceColors: true}
logger.Formatter = &logrus.TextFormatter{ForceColors: runtime.GOOS == "windows"}
return logger
}

View File

@ -25,6 +25,9 @@ type muxerMetrics struct {
outBoundRateCurr *prometheus.GaugeVec
outBoundRateMin *prometheus.GaugeVec
outBoundRateMax *prometheus.GaugeVec
compBytesBefore *prometheus.GaugeVec
compBytesAfter *prometheus.GaugeVec
compRateAve *prometheus.GaugeVec
}
type TunnelMetrics struct {
@ -187,6 +190,33 @@ func newMuxerMetrics() *muxerMetrics {
)
prometheus.MustRegister(outBoundRateMax)
compBytesBefore := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "comp_bytes_before",
Help: "Bytes sent via cross-stream compression, pre compression",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compBytesBefore)
compBytesAfter := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "comp_bytes_after",
Help: "Bytes sent via cross-stream compression, post compression",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compBytesAfter)
compRateAve := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "comp_rate_ave",
Help: "Average outbound cross-stream compression ratio",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compRateAve)
return &muxerMetrics{
rtt: rtt,
rttMin: rttMin,
@ -203,6 +233,9 @@ func newMuxerMetrics() *muxerMetrics {
outBoundRateCurr: outBoundRateCurr,
outBoundRateMin: outBoundRateMin,
outBoundRateMax: outBoundRateMax,
compBytesBefore: compBytesBefore,
compBytesAfter: compBytesAfter,
compRateAve: compRateAve,
}
}
@ -222,6 +255,9 @@ func (m *muxerMetrics) update(connectionID string, metrics *h2mux.MuxerMetrics)
m.outBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateCurr))
m.outBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMin))
m.outBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMax))
m.compBytesBefore.WithLabelValues(connectionID).Set(float64(metrics.CompBytesBefore.Value()))
m.compBytesAfter.WithLabelValues(connectionID).Set(float64(metrics.CompBytesAfter.Value()))
m.compRateAve.WithLabelValues(connectionID).Set(float64(metrics.CompRateAve()))
}
func convertRTTMilliSec(t time.Duration) float64 {

View File

@ -1,6 +1,7 @@
package origin
import (
"bufio"
"crypto/tls"
"fmt"
"io"
@ -28,36 +29,39 @@ import (
)
const (
dialTimeout = 15 * time.Second
dialTimeout = 15 * time.Second
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
TagHeaderNamePrefix = "Cf-Warp-Tag-"
DuplicateConnectionError = "EDUPCONN"
)
type TunnelConfig struct {
EdgeAddrs []string
OriginUrl string
Hostname string
OriginCert []byte
TlsConfig *tls.Config
ClientTlsConfig *tls.Config
Retries uint
HeartbeatInterval time.Duration
MaxHeartbeats uint64
ClientID string
BuildInfo *BuildInfo
ReportedVersion string
LBPool string
Tags []tunnelpogs.Tag
HAConnections int
HTTPTransport http.RoundTripper
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
ProtocolLogger *log.Logger
Logger *log.Logger
IsAutoupdated bool
GracePeriod time.Duration
RunFromTerminal bool
EdgeAddrs []string
OriginUrl string
Hostname string
OriginCert []byte
TlsConfig *tls.Config
ClientTlsConfig *tls.Config
Retries uint
HeartbeatInterval time.Duration
MaxHeartbeats uint64
ClientID string
BuildInfo *BuildInfo
ReportedVersion string
LBPool string
Tags []tunnelpogs.Tag
HAConnections int
HTTPTransport http.RoundTripper
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
ProtocolLogger *log.Logger
Logger *log.Logger
IsAutoupdated bool
GracePeriod time.Duration
RunFromTerminal bool
NoChunkedEncoding bool
WSGI bool
CompressionQuality uint64
}
type dialError struct {
@ -115,6 +119,7 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
OriginLocalIP: OriginLocalIP,
IsAutoupdated: c.IsAutoupdated,
RunFromTerminal: c.RunFromTerminal,
CompressionQuality: c.CompressionQuality,
}
}
@ -261,14 +266,14 @@ func ServeTunnel(
return castedErr.cause, !castedErr.permanent
case clientRegisterTunnelError:
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
raven.CaptureErrorAndWait(castedErr.cause, tags)
raven.CaptureError(castedErr.cause, tags)
return err, true
case muxerShutdownError:
logger.Infof("Muxer shutdown")
return err, true
default:
logger.WithError(err).Error("Serve tunnel error")
raven.CaptureErrorAndWait(err, tags)
raven.CaptureError(err, tags)
return err, true
}
}
@ -335,7 +340,7 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi
}
config.Logger.Info("Tunnel ID: " + registration.TunnelID)
config.Logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
return nil
}
@ -434,8 +439,9 @@ type TunnelHandler struct {
tags []tunnelpogs.Tag
metrics *TunnelMetrics
// connectionID is only used by metrics, and prometheus requires labels to be string
connectionID string
logger *log.Logger
connectionID string
logger *log.Logger
noChunkedEncoding bool
}
var dialer = net.Dialer{DualStack: true}
@ -451,13 +457,14 @@ func NewTunnelHandler(ctx context.Context,
return nil, "", fmt.Errorf("Unable to parse origin url %#v", originURL)
}
h := &TunnelHandler{
originUrl: originURL,
httpClient: config.HTTPTransport,
tlsConfig: config.ClientTlsConfig,
tags: config.Tags,
metrics: config.Metrics,
connectionID: uint8ToString(connectionID),
logger: config.Logger,
originUrl: originURL,
httpClient: config.HTTPTransport,
tlsConfig: config.ClientTlsConfig,
tags: config.Tags,
metrics: config.Metrics,
connectionID: uint8ToString(connectionID),
logger: config.Logger,
noChunkedEncoding: config.NoChunkedEncoding,
}
if h.httpClient == nil {
h.httpClient = http.DefaultTransport
@ -481,12 +488,13 @@ func NewTunnelHandler(ctx context.Context,
// Establish a muxed connection with the edge
// Client mux handshake with agent server
h.muxer, err = h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
Timeout: 5 * time.Second,
Handler: h,
IsClient: true,
HeartbeatInterval: config.HeartbeatInterval,
MaxHeartbeats: config.MaxHeartbeats,
Logger: config.ProtocolLogger.WithFields(log.Fields{}),
Timeout: 5 * time.Second,
Handler: h,
IsClient: true,
HeartbeatInterval: config.HeartbeatInterval,
MaxHeartbeats: config.MaxHeartbeats,
Logger: config.ProtocolLogger.WithFields(log.Fields{}),
CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality),
})
if err != nil {
return h, "", errors.New("TLS handshake error")
@ -528,13 +536,30 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
h.logResponse(response, cfRay, lbProbe)
}
} else {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if h.noChunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
}
}
response, err := h.httpClient.RoundTrip(req)
if err != nil {
h.logError(stream, err)
} else {
defer response.Body.Close()
stream.WriteHeaders(H1ResponseToH2Response(response))
io.Copy(stream, response.Body)
if h.isEventStream(response) {
h.writeEventStream(stream, response.Body)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
}
h.metrics.incrementResponses(h.connectionID, "200")
h.logResponse(response, cfRay, lbProbe)
}
@ -543,6 +568,25 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
return nil
}
func (h *TunnelHandler) writeEventStream(stream *h2mux.MuxedStream, responseBody io.ReadCloser) {
reader := bufio.NewReader(responseBody)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
break
}
stream.Write(line)
}
}
func (h *TunnelHandler) isEventStream(response *http.Response) bool {
if response.Header.Get("content-type") == "text/event-stream" {
h.logger.Debug("Detected Server-Side Events from Origin")
return true
}
return false
}
func (h *TunnelHandler) logError(stream *h2mux.MuxedStream, err error) {
h.logger.WithError(err).Error("HTTP request error")
stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "502"}})

View File

@ -55,7 +55,7 @@ func (cr *CertReloader) LoadCert() {
// Keep the old certificate if there's a problem reading the new one.
if err != nil {
raven.CaptureErrorAndWait(fmt.Errorf("Error parsing X509 key pair: %v", err), nil)
raven.CaptureError(fmt.Errorf("Error parsing X509 key pair: %v", err), nil)
return
}
cr.certificate = &cert

View File

@ -55,6 +55,7 @@ type RegistrationOptions struct {
OriginLocalIP string `capnp:"originLocalIp"`
IsAutoupdated bool `capnp:"isAutoupdated"`
RunFromTerminal bool `capnp:"runFromTerminal"`
CompressionQuality uint64 `capnp:"compressionQuality"`
}
func MarshalRegistrationOptions(s tunnelrpc.RegistrationOptions, p *RegistrationOptions) error {

View File

@ -41,6 +41,8 @@ struct RegistrationOptions {
isAutoupdated @8 :Bool;
# whether Argo Tunnel client is run from a terminal
runFromTerminal @9 :Bool;
# cross stream compression setting, 0 - off, 3 - high
compressionQuality @10 :UInt64;
}
struct Tag {

View File

@ -259,12 +259,12 @@ type RegistrationOptions struct{ capnp.Struct }
const RegistrationOptions_TypeID = 0xc793e50592935b4a
func NewRegistrationOptions(s *capnp.Segment) (RegistrationOptions, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6})
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 6})
return RegistrationOptions{st}, err
}
func NewRootRegistrationOptions(s *capnp.Segment) (RegistrationOptions, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6})
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 6})
return RegistrationOptions{st}, err
}
@ -430,12 +430,20 @@ func (s RegistrationOptions) SetRunFromTerminal(v bool) {
s.Struct.SetBit(25, v)
}
func (s RegistrationOptions) CompressionQuality() uint64 {
return s.Struct.Uint64(8)
}
func (s RegistrationOptions) SetCompressionQuality(v uint64) {
s.Struct.SetUint64(8, v)
}
// RegistrationOptions_List is a list of RegistrationOptions.
type RegistrationOptions_List struct{ capnp.List }
// NewRegistrationOptions creates a new list of RegistrationOptions.
func NewRegistrationOptions_List(s *capnp.Segment, sz int32) (RegistrationOptions_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 6}, sz)
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 6}, sz)
return RegistrationOptions_List{l}, err
}
@ -1245,89 +1253,92 @@ func (p TunnelServer_unregisterTunnel_Results_Promise) Struct() (TunnelServer_un
return TunnelServer_unregisterTunnel_Results{s}, err
}
const schema_db8274f9144abc7e = "x\xda\x9cU_\x88Te\x14?\xbf\xef\x9b\x99;\x0b" +
"\xbb\xce^\xee,\xe8\xa0\x0c\xc8.\xae\xd2\x9af\x96n" +
"\x7f\xf6O\xab5\xdb\xba\xce\xe7\xaca\xeaC\xd7\x99\xcf" +
"\xf1n3\xf7\x0e\xf7\xde1\x95\xd4\x12\xa5\x12\x92\xca|" +
"1\x0c\x15\x82\x8a\xa8\xa0 \x08\x03{\xc9\x07\x1f|\xa9" +
"(\x12\xa2\xc4\x87\xa4\x90\x96z(\x88\x1b\xdf\x9d\xbd3" +
"\xe3Zj\xbd\xdd\xfb\xfb\xcew\xce\xef\x9c\xef\x9c\xdfY" +
"\x91\xe0\xc3le\xfc\xfb8\x91\x18\x8b'\x82\x07\xab\x97" +
"\xce\xdew\xe2\xe2a\xd23,8pn<\xfd\x87\x7f" +
"\xe8;\"\xac\xfa\x8a\xed\x83\xf1\x13\xd3\x88\x8c\xabl#" +
"!\xb8t\xd7\xb9O_\xf9\xe8\x857H,\x01\x88b" +
"\x1a\xd1\xaa\xbf\xd8\x9f \x18:\x1f\"\x04\xaf\x7f\xf3\xd9" +
"d\xf5\xd5\x93gI_\x12\x9d\xaf\xe5\x8cQ,\xe8\xc9" +
"\xe1\xf2\xf9\x95\xb1O\x1a'q\xae\x8e\x06\xf85uu" +
"\x84\x7f@\x08\x16\xfd2\xdae_?t\x9e\xf4\x0cZ" +
",\x1a\x86?\xf0q\x18\xbf\xabO\xe3\xd7\xd0x|\xdb" +
"\xf1\xd7\xe2W\x8f_ \x91A\xbbuBY\xbf\x1cs" +
"a\x9c\x09\x83\x9f\x8a\xdd\xcf\x08A\xe6\xc3\x07\xde\x1f-" +
"}{q\x8e\xef0\xb3\x0em\xc6\xe8\xd1\xd4\x97\xae=" +
"C\x08\xd8Us\xc1s_?|\xb9-\x85\xaa\xf6#" +
"(\x16L>\xb1m\xbac\xff\x95+\xb3)@\x1d\x99" +
"Z\x98B]S\xd9\xaf~jDn_\xb3\xe5\x1a\xe9" +
"\x19~C!Oh\x830\xde\x0a\x83\x9c\xd1.\x18K" +
"\x93\x1aQp\xec\xc0\xd8\xc6\xb5\x8b?\x9fiw\xa7'" +
"g\x94\xbb\xbe\xa4r\xb7s\xcd\xcf\x8f\xf6\x1d\xfbbf" +
"\x0e\xeb\xd0p]r\x19\x8c\xcd\xca\x8f!\x94\xf1\xf5\xf5" +
"o~\x99Ie~\x9bS\x8f\x90~=9\x0d\xe3%" +
"e\xbb\xeaH2\x0b\x1a\x08\xfc\xbam\xcb\x8a[\x8b\x15" +
"\xef\x8e>\x8b\xcb\x8bf\xcd\xae\x0d\xae\xdbcy\xbee" +
"\x97\xa7B|(\xefT\xac\xe2\xde< :\xc1\x88\xf4" +
"E\x83D\x80\xde\xb3\x95\x08L\xd7G\x89\x86\xac\xb2\xed" +
"\xb82(Y^\xd1\xb1mI\xbc\xe8\x1f\xdcaVL" +
"\xbb(\x9b\x81\x127\x07j\x04(Hw\xb7t\x97\xd7" +
"mW\x96-\xcf\x97n\x03\xee\x1d\xca\x9b\xaeY\xf5D" +
"\x8c\xc7\x88b \xd2\xbbN\x12\x89n\x0e\xb1\x90!(" +
"\xbbfQ\xe6\xa5\x0b\xcb)M\x9a\xb6S\xe0\xb2\x888" +
"1\xc4\x09\xcd\xa0\xf3\xfek\xd0M\xd2\xabW|\x8f\x9a" +
"\xb7n}\x7f\xce\xed\xbc\x99\x0a)w6)\xaf\xdb\xaa" +
"&\x8cC\xe4\x19t \xadfF\xdf0N$&8" +
"\xc4\x16\x06\x9d\xb1tX\xd6\xcd\xa3D\"\xcf!\xb63" +
"\x04\x8ek\x95-\xfb\x11I\xdc\xf5\xd1E\x0c]\x84`" +
"\x97\xe3\xf9\xb6Y\x95D\x84Nb\xe8$\x1ctj\xbe" +
"\xe5\xd8\x1e\xba[\xf3@@w[\x09\xfe\xe1\x81G\xea" +
"\xfe.i\xfbV\xd1T\x97\x89\xc2\xb7mQ^L$" +
"\x869\xc4D\x1b\xe5\xdc=myD\x947\xech\xe5" +
"\xa1=-\xf7F\xac\xb2\xb2jZ\x95\xe8/Jf\x84" +
"\xb4\xc7[6\xb7\xe2\xb7)\xac\xaa\x1b\xb2\xdbX\xcb\x86" +
"\x19*\x8e\xfd\x11G\xa3\x03\xe3D\x85$8\x0ai\xb4" +
"h\x1a:F\x89\x0a\x9d\x0a\x9f\x8f\x16S\xa3\x07\x19\xa2" +
"B\xb7\xc2\x17\x82\x01<\x0dNd,\xc0\xbbD\x85\x85" +
"\x0a\xeeW\xe61\x9eF\x8c\xc8\xe8\x0b\xdd\xf7*|\x85" +
"\xc2\xe3\xb14\xe2D\xc6\x00\x96\x11\x15\xfa\x15>\xa6\xf0" +
"\x04K#Ad\x8c`\x9a\xa80\xac\xf0\x09\x85k\xf1" +
"\xb4\x1aQ#\x07\x97\xa8\xf0\x98\xc2\xa7\x14\x9e\x9c\x9fF" +
"R\xcdk\x88\xe7\x15\xbe]\xe1\x1d\x0b\xd2\xe8 2\x9e" +
"\xc4!\xa2\xc2\x16\x85\x97\xc0\x10\x14+\x96\xb4\xfd\\\xa9" +
"\xfd\xc5wK\xd7\xb3\x1c;\xfa\xe7\x8e\xd7,\xa9\x9c\x1d" +
"\\4\xda1\xef\xa4\xd4\xe4\"\xd5\x92w\x02R\x84\xa0" +
"\xe68\x95\xc9\x1b;)\xe5\x9be\x0f\xf3\x08y\x0et" +
"\xb7\xe4\x92\xa0\xc0 \x9c\xeb\xa2oQ\xca\xb1s%$" +
"\x88!\xd1|\xda\x09\x87\xb2E\xb3\x92\xab5\x99X\xde" +
"H\xddw\xea5\xca\x96L_\x96\x00b\x00!p\xeb" +
"\xf6z\xd7\xa9NA\xbaU\xcb6+\xd4<\x89z\x80" +
"\xcdm\x87lmp\xca,\xab\xe7O6[t\xe92" +
"\"\xd1\xcb!V\xb4\xb5\xe8\x80j\xd1~\x0eq/C" +
"J\xcdI\xb3\x1dw\x9b\x95\xba\xbc\xa9\xf1n'He" +
"\xe97\xber\xf6N\xa77o\xba\x9aY\xf5\xfe\xe7\xed" +
"M\xd2K)]i\x17\xb3A\"\x91\xe4\x10i\x86!" +
"7\x94\x1dt\xb7\x04\x7f\xce\x14\xf3\x7f\x0b7\xd4\x88\xd2" +
"\x18\xe18Qs\xcb\"Z.\xba\xd8GL\xcfih" +
"-6D{L\x7f\xc8%\xa6\xaf\xd6\xc0\x9a\x8b\x1d\xd1" +
"\x02\xd7\x97\x1e%\xa6\xf7iA$r4\xd4\x089\x8c" +
" \xca\x8e\xb2a~\xc3\x08\"%E$\x86D\xc3\xc8" +
"\xe3\xce\xcb}\x93\x10g\xbd;\xa9X\xb4\xf5n_\xaf" +
"F\x9c\x94\xe2\xab\xaa\xd5\xe6w\x9aHtr\x88\xf9\x0c" +
"A\xc5\x99U\xc5\xd4d[\x0b\xddJ\xad\x1a\x84#\xcd" +
"J\xa9\xcb\xca\x7f\xba\xe9\x7f\xbf\x12\xd4=\x1c\xe2p[" +
"\xb7>\xaf\xc0g9\xc4\x8bm\x82zD-\x86\xc3\x1c" +
"\xe2tS\xa3\xf4SG\x89\xc4i\x0e\xf1^K\xa0\xf4" +
"w\x94\xe1\xdb\x1c\xe2c\x06M\xban\xc4S\xab\xbb-" +
"\xdd\xad8\xe5\x09\xcb\x96\x9e\x9a\xf2\xd9\xc1VGj\x9c" +
"k\xd2\xad\x9a\xb6\xb4\xe1\xaf7\xadJ\xdd\x954w\x12" +
"scm\xe2\xf0w\x00\x00\x00\xff\xffk\x8c\xa4["
const schema_db8274f9144abc7e = "x\xda\x9cV]\x8c\x14\xd5\x12\xae\xef\x9c\x99\xedY\xb2" +
"\xcbl\xa7\x87\\\x98\\2\x09Y\xc2OX.\\\xe0" +
"f\xd9{\xaf\xfb\xe3\x82\x99uY\xa6\x190\x08\x98\xd0" +
"\xcc\x1c\x86^{\xba'\xdd=\x08D~$\x18\x95(" +
"Q\x91\x071\x18$1Qc\xfc\x89&\x06\x83\x09\xbe" +
"H\x0c1\xbc\xa8\x91Hb\x94\xec\x03DC\xdc\xe8\x83" +
"&\xa6\xcd\xe9\xd9\x9ei\x16\x05\xf4\xed\xccw\xeaT}" +
"U]\xf5\xd5,\xfb\x07\x1f`\xcb\x93\xdf$\x89\xf4\xe1" +
"d[\xf0\xbf\xea\xa53\xff9q\xf1\x08\xa9Y\x16\x1c" +
"87\x92\xf9\xc5?\xfc5\x11V|\xc1\xf6A\xbb\xc6" +
"\x14\"m\x82\xad'\x04\x97\x96\x9c\xfb\xf0\xd9\xf7\x9ex" +
"\x89\xf4\x05\x00QB!Z\xf1\x1b\xfb\x15\x04M\xe5\xfd" +
"\x84\xe0\x85\xaf>\x1a\xab>w\xf2\x0c\xa9\x0b\xa2\xfb\xd5" +
"\x9c1J\x04\xb3\xf2\xb8r~y\xe2\x83\xc6M\x92\xcb" +
"\xab\x1e~]>\x1d\xe4o\x13\x82\xb9?\x0cu\xda7" +
"\x0e\x9f'5\x8b\x16\x8b\x86\xe1\xb7|\x04\xda\xcf\xf2\xa8" +
"\xfd\x18\x1a\x8fl=\xfe|r\xe2\xf8\x05\xd2\xb3\x88q" +
"N\xb6I\xebg\x12.\xb4W\xc2\xe0\xa7\x12\x0e#\x04" +
"\xd9w\xfe\xfb\xd6P\xf9\xf2\xc5i\xbe\xc3\xcc\xd6)\x93" +
"\xda\x83\x8a<mR\x1e!\x04l\xc2\x98s\xe8\xcb{" +
"\xae\xc4RxW\xf9\x0e\x94\x08\xc6\x1e\xd8:\xde\xbe\xff" +
"\xea\xd5\xa9\x14 \xaf^U\xc2\x14\xce*2\xfbU\xdb" +
"\x07\xc5\xb6\xde\xcd\xd7I\xcd\xf2\x9b\x0ayY\xe9\x83v" +
"-\x0c2\xa1\\\xd0\xaa)\x85(8v`x\xfd\xea" +
"y\x1fO\xc6\xddmJMJwfJ\xba\xdb\xd9\xfb" +
"\xfd}\xf3\x8f}29\x8duh\xf8Tj1\xb4\x17" +
"\xa5\x1f\xed\x844\xbe\xb1\xf6\xe5\xcf\xb3\xe9\xecOz\x16" +
"q\xdb\x90\xfe\xd9\xd48\xb4\xcf\xa4\xed\x8aOS9P" +
"O\xe0\xd7m[Xn-Q\xfaWt,--\x19" +
"5\xbb\xd6\xb7f\x8f\xe9\xf9\xa6]\xd9\x18\xe2\xfd\x05\xc7" +
"2K{\x0b\x80\xde\x01F\xa4\xce\xed#\x02\xd4Y[" +
"\x88\xc0Tu\x88\xa8\xdf\xac\xd8\x8e+\x82\xb2\xe9\x95\x1c" +
"\xdb\x16\xc4K\xfe\xc1\x1d\x86e\xd8%\xd1\x0c\xd4vk" +
"\xa0F\x80\xa2pw\x0bwi\xddvE\xc5\xf4|\xe1" +
"6\xe0\xee\xfe\x82\xe1\x1aUOO\xf0\x04Q\x02Dj" +
"\xe7I\"\xbd\x8bC\xff'CPq\x8d\x92(\x08\x17" +
"\xa6S\x1e3l\xa7\xc8E\x09IbH\x12\x9aAg" +
"\xfe\xd5\xa0\x1b\x84W\xb7|\x8f\x9a\xafn\xff~\xda\xeb" +
"\x82\x91\x0e)w4)\xaf\xd9\"'\x8cC/0\xa8" +
"@F\xce\x8c\xban\x84H\x1f\xe5\xd073\xa8\x8ce" +
"\xc2\xb2n\x1a\"\xd2\x0b\x1c\xfa6\x86\xc0q\xcd\x8ai" +
"\xdf+\x88\xbb>:\x89\xa1\x93\x10\xecr<\xdf6\xaa" +
"\x82\x88\xd0A\x0c\x1d\x84\x83N\xcd7\x1d\xdbCWk" +
"\x1e\x08\xe8\x8a\x95\xe0\x0f>\xf0`\xdd\xdf%l\xdf," +
"\x19\xf21Q\xf8m[\x94\xe7\x11\xe9\x03\x1c\xfah\x8c" +
"r\xfe\xdf\xb1<\"\xca\xebv\xb4\xf2P\x1e\x16{#" +
"V9Q5L+\xfa\x15%3H\xca\xfd-\x9b\xdb" +
"\xf1\xdb\x10V\xd5\x0d\xd9\xad\xaf\xe5\xc2\x0c%\xc7%\x11" +
"Gm>F\x88\x8a\xdd\xe0(.C\x8b\xa6\xd6\x83!" +
"\xa2\xe2B\x89\xafD\x8b\xa9\xb6\x1cY\xa2\xe2\x12\x89\xf7" +
"\x82\x01<\x03N\xa4\xad\xc2\x1bD\xc5^\x09\x0fK\xf3" +
"\x04\xcf A\xa4\x0d\x86\xee\x07$>*\xf1d\"\x83" +
"$\x91\x96\xc7b\xa2\xe2\xb0\xc4\xb7K\xbc\x8de\xd0F" +
"\xa4=\x84q\xa2\xe26\x89\xef\x92\xb8\x92\xcc\xc8\x11\xd5" +
"\x04\\\xa2bY\xe25\x89\xa7fg\x90\"\xd2\xaa!" +
"nI|\x8f\xc4\xdb\xe7d\xd0N\xa4\xd5q\x98\xa8\xe8" +
"K\xfc\x90\xc4g \x83\x19D\xda~\x9c$*\x1e\x92" +
"\xf8\xd3`\x08J\x96)l?_\x8ew\xc2n\xe1z" +
"\xa6cG\xbf\xb9\xe35K-\xa6\x06\x1a\x8d6-8" +
"i9\xd1H\xb7d\x9f\x804!\xa89\x8e5vs" +
"\x87\xa5}\xa3\xe2a&\xa1\xc0\x81\xae\x96\x8c\x12$\x18" +
"\x84\xf3^\xf2MJ;v\xbe\x8c6bhk~\xf2" +
"Q\x87r%\xc3\xca\xd7\x9aLLo\xb0\xee;\xf5\x1a" +
"\xe5\xca\x86/\xca\x001\x80\x10\xb8u{\xad\xebT7" +
"B\xb8U\xd36,j\xde\x94\x9cj\xcd\x15\x9e\x07\xd3" +
"\xb1\xf5\xbaa\x99\xdc\xdf\x8bvbh\x8f\xf5\x10\x9b\xde" +
"C\xb9Z\xdfF\xa3\"{&\xd5\xec\xebE\x8b\x89\xf4" +
"n\x0e}Y\xac\xaf{d_/\xe4\xd0W2\xa4\xe5" +
"p5{x\xb7a\xd5\xc5-\xddz'\x15\xab\x08\xbf" +
"q\xca\xdb;\x9d\xee\x82\xe1*F\xd5\xfb\x9b\xaf7\x08" +
"/-\xc5(\xae\x80}Dz\x8aC\xcf0\xf4\xbb\xa1" +
"V\xa1\xab\xb5%\xa6\x8d>\xff\xb3p\xfd\x8d(\x8d\xb9" +
"O\x125W3\xa2\x8d\xa4\xea\xfb\x88\xa9y\x05\xadm" +
"\x88h\xf9\xa9\xffw\x89\xa9\xab\x14\xb0\xe6\xbf\x01D[" +
"_]t\x94\x98:_\x09\"e\xa4\xfeF\xc8\x01\x04" +
"Qv\x94\x0b\xf3\x1b@\x10\xc9/\"\x05%\x1a@\x01" +
"w_\xee[\xd4;\xe7\xddM\xc5\xa2Uy\xe7z5" +
"\xe2\xa4%_Y\xad\x98\xdfq\"\xbd\x83C\x9f\xcd\x10" +
"X\xce\x94\x94\xa6\xc7b-t;\x89k\x10\x8e\x84." +
"-\x1fK\xff\x99\xa6\xff\xfdR\x85\xf7p\xe8Gb\xdd" +
"\xfa\x98\x04\x1f\xe5\xd0\x9f\x8c\xa9\xf0\xe3r\x9b\x1c\xe1\xd0" +
"O7\x85M=u\x94H?\xcd\xa1\xbf\xd9R5\xf5" +
"ui\xf8\x1a\x87\xfe>\x83\"\\7\xe2\xa9\xd4\xdd\x96" +
"X[Ne\xd4\xb4\x85'%`j\xea\xe5\x95\x9c\xf5" +
"\x9ap\xab\x86-l\xf8k\x0d\xd3\xaa\xbb\x82Zc\xda" +
"\xc8/?\x1cS\x8e\xdf\x03\x00\x00\xff\xff#\xa3\xb8\x8f"
func init() {
schemas.Register(schema_db8274f9144abc7e,