├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── daemon.go ├── dsp ├── Makefile └── main.go ├── dsplib ├── Makefile ├── campaign.go └── dsp.go ├── example.html ├── main.go ├── main_test.go ├── mux.go ├── openrtb └── rtb.go ├── ssp.json ├── ssp ├── Makefile ├── auction.go ├── auction_test.go ├── bid.go ├── bid_test.go ├── dsp.go ├── dsp_test.go ├── id.go ├── placement.go └── testdsp.go ├── static.go ├── static ├── html5vast.css └── html5vast.js └── vendor ├── github.com └── julienschmidt │ └── httprouter │ ├── LICENSE │ ├── README.md │ ├── path.go │ ├── router.go │ └── tree.go └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | /my_first_ssp 2 | /dsp/dsp 3 | /dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: ssp 3 | goos: 4 | - linux 5 | - darwin 6 | goarch: 7 | - amd64 8 | archive: 9 | files: 10 | - LICENSE 11 | - ssp.json 12 | wrap_in_directory: true 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Harmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test build static release 2 | 3 | all: test build 4 | 5 | test: 6 | go test ./... 7 | 8 | build: 9 | go build -i ./... 10 | go build -o ./my_first_ssp 11 | 12 | static: 13 | go get github.com/mjibson/esc 14 | esc -o static.go --prefix static/ static/ 15 | $(MAKE) build 16 | 17 | release: 18 | go get -v github.com/goreleaser/goreleaser 19 | goreleaser --rm-dist # --snapshot 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Project status**: Please note that this project works, but is otherwise left as-is. I won't maintain this, and I can't provide any support. 2 | 3 | 4 | # Minimal SSP 5 | 6 | A very basic openrtb2.3 SSP used to tests RTB campaigns. It has a few placements 7 | and a few RTB backends configured via a JSON file, and you can see how the 8 | placements behave in a browser. Win-notification URLs are called, but the 9 | system is stateless, so there is no big "money spent" counter. 10 | Basic support for VAST3 video. 11 | 12 | # Auction 13 | 14 | It's a proper second-price auction, and if there is only a single bid it'll 15 | charge the bid floor for the placement. 16 | -------------------------------------------------------------------------------- /daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/alicebob/ssp/ssp" 11 | ) 12 | 13 | const ( 14 | timeout = 100 * time.Millisecond 15 | cookieName = "uid" 16 | ) 17 | 18 | type Daemon struct { 19 | BaseURL string 20 | DSPs []ssp.DSP 21 | } 22 | 23 | func NewDaemon(base string, dsps []ssp.DSP) *Daemon { 24 | return &Daemon{ 25 | BaseURL: base, 26 | DSPs: dsps, 27 | } 28 | } 29 | 30 | // RunAuction for a placement. Can take up to $timeout to run. 31 | func (d *Daemon) RunAuction(pl *ssp.Placement, r *http.Request, userID string) *ssp.Auction { 32 | a := ssp.NewAuction() 33 | a.UserAgent = r.UserAgent() 34 | if addr := r.RemoteAddr; addr != "" { 35 | a.IP, _, _ = net.SplitHostPort(addr) 36 | } 37 | a.UserID = userID 38 | a.PlacementID = pl.ID 39 | a.PlacementType = pl.Type 40 | a.FloorCPM = pl.FloorCPM 41 | a.Width = pl.Width 42 | a.Height = pl.Height 43 | 44 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 45 | defer cancel() 46 | won, err := ssp.RunAuction(ctx, d.DSPs, a) 47 | if err != nil { 48 | log.Printf("bid error: %v", err) 49 | return a 50 | } 51 | if won == nil { 52 | return nil 53 | } 54 | log.Printf("winning bid: %s: %f", won.SSPID, won.PriceCPM) 55 | a.PriceCPM = won.PriceCPM 56 | a.NotificationURL = won.NotificationURL 57 | a.AdMarkup = won.AdMarkup 58 | a.Won() 59 | return a 60 | } 61 | 62 | func getUserID(r *http.Request) string { 63 | if c, err := r.Cookie(cookieName); err == nil && c != nil { 64 | return c.Value 65 | } 66 | return ssp.RandomID(10) 67 | } 68 | 69 | func userID(w http.ResponseWriter, r *http.Request) string { 70 | userID := getUserID(r) 71 | http.SetCookie(w, &http.Cookie{ 72 | Name: cookieName, 73 | Value: userID, 74 | Path: "/", 75 | MaxAge: 100 * 24 * 60 * 60, 76 | HttpOnly: true, 77 | }) 78 | return userID 79 | } 80 | -------------------------------------------------------------------------------- /dsp/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | go build 5 | -------------------------------------------------------------------------------- /dsp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/alicebob/ssp/dsplib" 7 | ) 8 | 9 | const listen = "localhost:9990" 10 | 11 | var ( 12 | campaigns = []dsplib.Campaign{ 13 | dsplib.Campaign{ 14 | ID: "camp1", 15 | Type: "banner", 16 | Width: 466, 17 | Height: 214, 18 | BidCPM: 0.43, 19 | ImageURL: "https://imgs.xkcd.com/comics/debugger.png", 20 | ClickURL: "https://xkcd.com/1163/", 21 | }, 22 | dsplib.Campaign{ 23 | ID: "camp2", 24 | Type: "banner", 25 | Width: 300, 26 | Height: 330, 27 | BidCPM: 0.12, 28 | ImageURL: "https://imgs.xkcd.com/comics/duty_calls.png", 29 | ClickURL: "https://xkcd.com/386/", 30 | }, 31 | dsplib.Campaign{ 32 | ID: "vid1", 33 | Type: "video", 34 | Width: 400, 35 | Height: 400, 36 | BidCPM: 0.8, 37 | VideoURL: "http://techslides.com/demos/sample-videos/small.mp4", 38 | ClickURL: "https://xkcd.com/386/", 39 | }, 40 | } 41 | ) 42 | 43 | func main() { 44 | s := dsplib.NewDSP(listen, campaigns) 45 | defer s.Close() 46 | log.Printf("configured campaigns:") 47 | for _, c := range campaigns { 48 | log.Printf(" - %s %dx%d: %s ($%.2f)", c.Type, c.Width, c.Height, c.ID, c.BidCPM) 49 | } 50 | log.Printf("BidURL: %s", s.BidURL) 51 | for { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dsplib/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | go build -i 5 | -------------------------------------------------------------------------------- /dsplib/campaign.go: -------------------------------------------------------------------------------- 1 | package dsplib 2 | 3 | type Campaign struct { 4 | ID string 5 | Type string 6 | Width, Height int 7 | ImageURL string 8 | VideoURL string 9 | ClickURL string 10 | BidCPM float64 11 | } 12 | -------------------------------------------------------------------------------- /dsplib/dsp.go: -------------------------------------------------------------------------------- 1 | // Basic test DSP 2 | package dsplib 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "strconv" 11 | "sync" 12 | 13 | "github.com/julienschmidt/httprouter" 14 | 15 | "github.com/alicebob/ssp/openrtb" 16 | ) 17 | 18 | type DSP struct { 19 | BaseURL string 20 | BidURL string 21 | server *http.Server 22 | campaigns []Campaign 23 | wonCount int 24 | wonCPM float64 25 | mu sync.Mutex 26 | } 27 | 28 | func NewDSP(listen string, cs []Campaign) *DSP { 29 | l, err := net.Listen("tcp", listen) 30 | if err != nil { 31 | panic(err.Error()) 32 | } 33 | port := l.Addr().(*net.TCPAddr).Port 34 | base := fmt.Sprintf("http://localhost:%d/", port) 35 | d := &DSP{ 36 | BaseURL: base, 37 | BidURL: fmt.Sprintf("%srtb", base), 38 | campaigns: cs, 39 | } 40 | d.server = &http.Server{ 41 | Addr: listen, 42 | Handler: d.Mux(), 43 | } 44 | go d.server.Serve(l) 45 | return d 46 | } 47 | 48 | func (dsp *DSP) Close() error { 49 | return dsp.server.Close() 50 | } 51 | 52 | func (dsp *DSP) Mux() *httprouter.Router { 53 | r := httprouter.New() 54 | r.POST("/rtb", dsp.rtbHandler()) 55 | r.GET("/win", dsp.winHandler()) 56 | return r 57 | } 58 | 59 | func (dsp *DSP) rtbHandler() httprouter.Handle { 60 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 61 | log.Printf("RTB request") 62 | var req openrtb.BidRequest 63 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 64 | log.Printf("req: %s", err) 65 | http.Error(w, http.StatusText(400), 400) 66 | return 67 | } 68 | 69 | // log.Printf("RTB request: %#v", req) 70 | var bids []openrtb.Bid 71 | for _, imp := range req.Impressions { 72 | bids = append(bids, dsp.makeBid(imp)...) 73 | } 74 | if len(bids) == 0 { 75 | log.Printf("no bids") 76 | w.WriteHeader(204) 77 | fmt.Fprintf(w, "{}") 78 | return 79 | } 80 | res := openrtb.BidResponse{ 81 | ID: req.ID, 82 | Seatbids: []openrtb.Seatbid{ 83 | { 84 | Bids: bids, 85 | }, 86 | }, 87 | } 88 | pl, err := json.Marshal(res) 89 | if err != nil { 90 | log.Printf("req: %s", err) 91 | http.Error(w, http.StatusText(500), 500) 92 | return 93 | } 94 | w.Header().Set("Content-Type", "application/json") 95 | if n, err := w.Write(pl); err != nil || n != len(pl) { 96 | log.Printf("req: %s (n: %d/%d)", err, n, len(pl)) 97 | } 98 | } 99 | } 100 | 101 | // place a bid on every campaign which matches. 102 | func (dsp *DSP) makeBid(imp openrtb.Impression) []openrtb.Bid { 103 | var bids []openrtb.Bid 104 | for _, c := range dsp.campaigns { 105 | switch { 106 | case imp.Banner != nil && (c.Type == "banner" || c.Type == ""): 107 | b := imp.Banner 108 | if b.Width == c.Width && b.Height == c.Height { 109 | bids = append( 110 | bids, 111 | openrtb.Bid{ 112 | ImpressionID: imp.ID, 113 | Price: c.BidCPM, 114 | AdMarkup: fmt.Sprintf( 115 | ``, 116 | c.ClickURL, 117 | c.ImageURL, 118 | c.Width, 119 | c.Height, 120 | ), 121 | NotificationURL: dsp.winURL(), 122 | }, 123 | ) 124 | } 125 | case imp.Video != nil && c.Type == "video": 126 | // TODO: fuzzier dimension matching 127 | v := imp.Video 128 | if v.Width == c.Width && v.Height == c.Height { 129 | bids = append( 130 | bids, 131 | openrtb.Bid{ 132 | ImpressionID: imp.ID, 133 | Price: c.BidCPM, 134 | AdMarkup: vast30(c), 135 | NotificationURL: dsp.winURL(), 136 | }, 137 | ) 138 | } 139 | } 140 | } 141 | return bids 142 | } 143 | 144 | func (dsp *DSP) winHandler() httprouter.Handle { 145 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 146 | pr := r.FormValue("p") 147 | if pr == "" { 148 | log.Printf("no price") 149 | http.Error(w, http.StatusText(400), 400) 150 | return 151 | } 152 | price, err := strconv.ParseFloat(pr, 64) 153 | if err != nil { 154 | log.Printf("invalid price %q: %s", pr, err) 155 | http.Error(w, http.StatusText(400), 400) 156 | return 157 | } 158 | log.Printf("won %f", price) 159 | dsp.mu.Lock() 160 | defer dsp.mu.Unlock() 161 | dsp.wonCount++ 162 | dsp.wonCPM += price 163 | } 164 | } 165 | 166 | func (dsp *DSP) winURL() string { 167 | return fmt.Sprintf("%swin?p=${AUCTION_PRICE}", dsp.BaseURL) 168 | } 169 | 170 | // Won returns count+total CPM of winnotices 171 | func (dsp *DSP) Won() (int, float64) { 172 | dsp.mu.Lock() 173 | defer dsp.mu.Unlock() 174 | return dsp.wonCount, dsp.wonCPM 175 | } 176 | 177 | func vast30(c Campaign) string { 178 | return fmt.Sprintf(`My First SSP%s00:01:00.000%s`, 179 | c.ID, 180 | c.ID, 181 | c.Width, 182 | c.Height, 183 | c.VideoURL, 184 | c.ClickURL, 185 | ) 186 | } 187 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | Example page 3 | 4 | hello world
5 | 6 | 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/alicebob/ssp/ssp" 11 | ) 12 | 13 | type Config struct { 14 | DSPs []ssp.DSP `json:"dsps"` 15 | Placements []ssp.Placement `json:"placements"` 16 | } 17 | 18 | var ( 19 | config = flag.String("config", "./ssp.json", "config file") 20 | listen = flag.String("listen", ":9998", "listen") 21 | public = flag.String("public", "http://localhost:9998", "public") 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | f, err := ioutil.ReadFile(*config) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | var c Config 32 | if err := json.Unmarshal(f, &c); err != nil { 33 | log.Fatal(err) 34 | } 35 | log.Printf("configured DSPs:") 36 | for _, d := range c.DSPs { 37 | log.Printf(" - %s", d.Name) 38 | } 39 | s := NewDaemon(*public, c.DSPs) 40 | log.Printf("listening on %s...", *listen) 41 | log.Fatal(http.ListenAndServe(*listen, mux(s, c.Placements))) 42 | } 43 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/cookiejar" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/julienschmidt/httprouter" 15 | 16 | "github.com/alicebob/ssp/dsplib" 17 | "github.com/alicebob/ssp/openrtb" 18 | "github.com/alicebob/ssp/ssp" 19 | ) 20 | 21 | var ( 22 | camp1 = dsplib.Campaign{ 23 | ID: "camp1", 24 | Type: "banner", 25 | Width: 466, 26 | Height: 214, 27 | BidCPM: 0.43, 28 | ImageURL: "https://imgs.xkcd.com/comics/debugger.png", 29 | ClickURL: "https://xkcd.com/1163/", 30 | } 31 | camp2 = dsplib.Campaign{ 32 | ID: "camp2", 33 | Type: "banner", 34 | Width: 300, 35 | Height: 330, 36 | BidCPM: 0.12, 37 | ImageURL: "https://imgs.xkcd.com/comics/duty_calls.png", 38 | ClickURL: "https://xkcd.com/386/", 39 | } 40 | pl1 = ssp.Placement{ 41 | ID: "my_website_1", 42 | Type: ssp.Banner, 43 | Name: "My Website", 44 | FloorCPM: 0.2, 45 | Width: 466, 46 | Height: 214, 47 | } 48 | ) 49 | 50 | func TestMain(t *testing.T) { 51 | jar, _ := cookiejar.New(nil) 52 | cl := &http.Client{ 53 | Jar: jar, 54 | } 55 | 56 | dsp1, s1 := ssp.RunDSP("dsp1", "My First DSP") 57 | defer s1.Close() 58 | dsp2, s2 := ssp.RunDSP("dsp2", "My Second DSP", camp1, camp2) 59 | defer s2.Close() 60 | 61 | d := NewDaemon("http://localhost/", []ssp.DSP{dsp1, dsp2}) 62 | s := httptest.NewServer(mux(d, []ssp.Placement{pl1})) 63 | defer s.Close() 64 | 65 | { 66 | r := getok(t, s, cl, 200, "/") 67 | if want := "My Website"; !strings.Contains(r, want) { 68 | t.Errorf("not found: %q", want) 69 | } 70 | } 71 | 72 | { 73 | r := getok(t, s, cl, 200, "/p/my_website_1/code.html") 74 | if want := " 119 | Placement list 120 | 121 | Available placements:
122 |
123 | {{range .}} 124 | {{.Name}}
125 | {{end}} 126 | `)) 127 | 128 | exampleTemplate = template.Must(template.New("list").Parse(` 129 | 130 | {{ .name }} 131 | 132 | {{ .name}}
133 | {{ .width }}x{{ .height }}
134 |
135 |
136 | 137 | Embed code:
138 |
139 | 	{{ .code | html }}
140 | 
141 |
142 | raw 143 |
144 |
145 |
146 | 147 |
148 | {{ .code }} 149 |
150 | `)) 151 | ) 152 | -------------------------------------------------------------------------------- /openrtb/rtb.go: -------------------------------------------------------------------------------- 1 | package openrtb 2 | 3 | // minimal OpenRTB 2.3 4 | 5 | type BidRequest struct { 6 | ID string `json:"id"` 7 | Impressions []Impression `json:"imp,omitempty"` 8 | Device Device `json:"device"` 9 | User User `json:"user"` 10 | } 11 | 12 | type Impression struct { 13 | ID string `json:"id,omitempty"` 14 | Banner *Banner `json:"banner,omitempty"` 15 | Video *Video `json:"video,omitempty"` 16 | Bidfloor float64 `json:"bidfloor,omitempty"` 17 | BidfloorCur string `json:"bidfloorcur,omitempty"` 18 | Secure int `json:"secure,omitempty"` 19 | } 20 | 21 | type Banner struct { 22 | Width int `json:"w,omitempty"` 23 | Height int `json:"h,omitempty"` 24 | } 25 | 26 | type Video struct { 27 | Width int `json:"w,omitempty"` 28 | Height int `json:"h,omitempty"` 29 | Mimes []string `json:"mimes,omitempty"` 30 | } 31 | 32 | type Device struct { 33 | UserAgent string `json:"ua,omitempty"` 34 | IP string `json:"ip,omitempty"` 35 | } 36 | 37 | type User struct { 38 | ID string `json:"id,omitempty"` 39 | } 40 | 41 | type BidResponse struct { 42 | ID string `json:"id,omitempty"` 43 | Seatbids []Seatbid `json:"seatbid,omitempty"` 44 | } 45 | 46 | type Seatbid struct { 47 | Bids []Bid `json:"bid,omitempty"` 48 | } 49 | 50 | type Bid struct { 51 | ImpressionID string `json:"impid,omitempty"` 52 | Price float64 `json:"price,omitempty"` 53 | AdMarkup string `json:"adm,omitempty"` 54 | NotificationURL string `json:"nurl,omitempty"` 55 | } 56 | -------------------------------------------------------------------------------- /ssp.json: -------------------------------------------------------------------------------- 1 | { 2 | "dsps": [ 3 | { 4 | "id": "dsp1", 5 | "name": "Test 1", 6 | "bid_url": "http://localhost:9990/rtb" 7 | }, 8 | { 9 | "id": "dsp2", 10 | "name": "Test 2", 11 | "bid_url": "http://localhost:9999/..." 12 | } 13 | ], 14 | "placements": [ 15 | { 16 | "id": "example32050", 17 | "type": "banner", 18 | "name": "Example 320x50", 19 | "floor_cpm": 0.1, 20 | "width": 320, 21 | "height": 50 22 | }, 23 | { 24 | "id": "example300250", 25 | "type": "banner", 26 | "name": "Example 300x250", 27 | "floor_cpm": 0.1, 28 | "width": 300, 29 | "height": 250 30 | }, 31 | { 32 | "id": "example320480", 33 | "type": "banner", 34 | "name": "Example 320x480", 35 | "floor_cpm": 0.1, 36 | "width": 320, 37 | "height": 480 38 | }, 39 | { 40 | "id": "example72890", 41 | "type": "banner", 42 | "name": "Example 728x90", 43 | "floor_cpm": 0.1, 44 | "width": 728, 45 | "height": 90 46 | }, 47 | { 48 | "id": "video400400", 49 | "type": "video", 50 | "name": "Video 400x400", 51 | "floor_cpm": 0.1, 52 | "width": 400, 53 | "height": 400 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /ssp/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | go test 5 | -------------------------------------------------------------------------------- /ssp/auction.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | Currency = "USD" 12 | ) 13 | 14 | type Auction struct { 15 | ID string 16 | PlacementID string 17 | PlacementType Type 18 | UserID string 19 | FloorCPM float64 20 | Width, Height int 21 | UserAgent string 22 | IP string 23 | PriceCPM float64 24 | AdMarkup string 25 | NotificationURL string 26 | } 27 | 28 | func NewAuction() *Auction { 29 | return &Auction{ 30 | ID: RandomID(42), 31 | } 32 | } 33 | 34 | // Won will call the notication callback, if any 35 | func (a *Auction) Won() { 36 | n := a.NotificationURL 37 | if n == "" { 38 | return 39 | } 40 | url := strings.Replace(n, "${AUCTION_PRICE}", fmt.Sprintf("%0.2f", a.PriceCPM), -1) 41 | go func() { 42 | // TODO: retry? 43 | res, err := http.Get(url) 44 | if err != nil { 45 | log.Printf("notification err: %s", err) 46 | return 47 | } 48 | defer res.Body.Close() 49 | }() 50 | } 51 | -------------------------------------------------------------------------------- /ssp/auction_test.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestAuction(t *testing.T) { 9 | pl := Placement{ 10 | ID: "pl1", 11 | } 12 | a := NewAuction() 13 | a.UserAgent = "mozilla 1.2.3" 14 | a.IP = "1.2.3.4" 15 | a.PlacementID = pl.ID 16 | a.AdMarkup = `` 17 | a.NotificationURL = "https://my.example/winme.cgi" 18 | html, err := pl.Iframe(a) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if want := "image.png"; !strings.Contains(html, want) { 23 | t.Fatalf("%q not found", want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ssp/bid.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "sort" 7 | "sync" 8 | ) 9 | 10 | type Bid struct { 11 | SSPID string 12 | PriceCPM float64 13 | NotificationURL string 14 | Type Type 15 | AdMarkup string // VAST also goes here 16 | } 17 | 18 | func RunAuction(ctx context.Context, dsps []DSP, a *Auction) (*Bid, error) { 19 | var ( 20 | wg sync.WaitGroup 21 | bids = make(chan Bid) 22 | ) 23 | for _, dsp := range dsps { 24 | wg.Add(1) 25 | go func(dsp DSP) { 26 | defer wg.Done() 27 | bs, err := dsp.Bid(ctx, a) 28 | if err != nil { 29 | log.Printf("dsp %s bid err: %s", dsp.ID, err) 30 | } 31 | for _, b := range bs { 32 | bids <- b 33 | } 34 | }(dsp) 35 | } 36 | go func() { 37 | wg.Wait() 38 | close(bids) 39 | }() 40 | 41 | var allBids []Bid 42 | for b := range bids { 43 | // TODO: match impression slots 44 | if b.PriceCPM < a.FloorCPM { 45 | continue 46 | } 47 | allBids = append(allBids, b) 48 | } 49 | won := pickBid(allBids) 50 | if won != nil && won.PriceCPM == 0 { 51 | won.PriceCPM = a.FloorCPM 52 | } 53 | return won, nil 54 | } 55 | 56 | // pickBid with a second price auction. Will return a 0 price if there is only 57 | // one auction. Does not check validness of any bid. 58 | func pickBid(bs []Bid) *Bid { 59 | if len(bs) == 0 { 60 | return nil 61 | } 62 | sort.Slice(bs, func(i, j int) bool { 63 | if bs[i].PriceCPM == bs[j].PriceCPM { 64 | return bs[i].SSPID > bs[j].SSPID 65 | } 66 | return bs[i].PriceCPM > bs[j].PriceCPM 67 | }) 68 | won := bs[0] 69 | won.PriceCPM = 0.0 70 | if len(bs) > 1 { 71 | won.PriceCPM = bs[1].PriceCPM 72 | } 73 | return &won 74 | } 75 | -------------------------------------------------------------------------------- /ssp/bid_test.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPickBidSimple(t *testing.T) { 8 | bs := []Bid{ 9 | { 10 | SSPID: "ssp1", 11 | PriceCPM: 0.4, 12 | }, 13 | { 14 | SSPID: "ssp2", 15 | PriceCPM: 0.6, // 2nd price 16 | }, 17 | { 18 | SSPID: "ssp3", 19 | PriceCPM: 0.8, // Winner 20 | }, 21 | } 22 | if have, want := *pickBid(bs), (Bid{ 23 | SSPID: "ssp3", 24 | PriceCPM: 0.6, 25 | }); have != want { 26 | t.Errorf("have %+v, want %+v", have, want) 27 | } 28 | } 29 | 30 | func TestPickBidNoBid(t *testing.T) { 31 | bs := []Bid{} 32 | if have, want := pickBid(bs), (*Bid)(nil); have != want { 33 | t.Errorf("have %+v, want %+v", have, want) 34 | } 35 | } 36 | 37 | func TestPickBidSingle(t *testing.T) { 38 | bs := []Bid{ 39 | { 40 | SSPID: "ssp1", 41 | PriceCPM: 0.4, 42 | }, 43 | } 44 | if have, want := *pickBid(bs), (Bid{ 45 | SSPID: "ssp1", 46 | PriceCPM: 0.0, // no second bid 47 | }); have != want { 48 | t.Errorf("have %+v, want %+v", have, want) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ssp/dsp.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/alicebob/ssp/openrtb" 12 | ) 13 | 14 | var ( 15 | ErrNoBid = errors.New("no bid") 16 | ) 17 | 18 | type DSP struct { 19 | ID string `json:"id"` 20 | Name string `json:"name"` 21 | BidURL string `json:"bid_url"` 22 | } 23 | 24 | func (d *DSP) Bid(ctx context.Context, a *Auction) ([]Bid, error) { 25 | imp := openrtb.Impression{ 26 | ID: "1", 27 | Bidfloor: a.FloorCPM, 28 | BidfloorCur: Currency, 29 | Secure: 1, 30 | } 31 | switch a.PlacementType { 32 | case Banner: 33 | imp.Banner = &openrtb.Banner{ 34 | Width: a.Width, 35 | Height: a.Height, 36 | } 37 | case Video: 38 | imp.Video = &openrtb.Video{ 39 | Width: a.Width, 40 | Height: a.Height, 41 | Mimes: []string{"video/mp4"}, 42 | } 43 | default: 44 | return nil, fmt.Errorf("unsupported auction type: %s", a.PlacementType) 45 | } 46 | rtb := openrtb.BidRequest{ 47 | ID: a.ID, 48 | Impressions: []openrtb.Impression{ 49 | imp, 50 | }, 51 | Device: openrtb.Device{ 52 | UserAgent: a.UserAgent, 53 | IP: a.IP, 54 | }, 55 | User: openrtb.User{ 56 | ID: a.UserID, 57 | }, 58 | } 59 | pl, err := json.Marshal(rtb) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | cl := &http.Client{} 65 | cl.CheckRedirect = func(req *http.Request, via []*http.Request) error { 66 | return http.ErrUseLastResponse 67 | } 68 | 69 | req, err := http.NewRequest("POST", d.BidURL, bytes.NewBuffer(pl)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | req = req.WithContext(ctx) 74 | 75 | resp, err := cl.Do(req) 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer req.Body.Close() 80 | 81 | if err := ctx.Err(); err != nil { 82 | return nil, err 83 | } 84 | 85 | switch s := resp.StatusCode; s { 86 | case 204: 87 | return nil, ErrNoBid 88 | case 200: 89 | // pass 90 | default: 91 | return nil, fmt.Errorf("unexpected HTTP status code: %d %s", s, resp.Status) 92 | } 93 | var bid openrtb.BidResponse 94 | if err := json.NewDecoder(resp.Body).Decode(&bid); err != nil { 95 | return nil, err 96 | } 97 | var bids []Bid 98 | for _, s := range bid.Seatbids { 99 | for _, b := range s.Bids { 100 | bids = append(bids, Bid{ 101 | SSPID: d.ID, 102 | PriceCPM: b.Price, 103 | AdMarkup: b.AdMarkup, 104 | NotificationURL: b.NotificationURL, 105 | }) 106 | } 107 | } 108 | return bids, nil 109 | } 110 | -------------------------------------------------------------------------------- /ssp/dsp_test.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/alicebob/ssp/dsplib" 9 | ) 10 | 11 | func TestDSPNoBid(t *testing.T) { 12 | dsp, s := RunDSP("dsp", "My Second DSP", dsplib.Campaign{ 13 | Width: 400, Height: 500, 14 | }) 15 | defer s.Close() 16 | 17 | a := NewAuction() 18 | a.UserAgent = "chromium 4.5.6" 19 | a.IP = "5.6.7.8" 20 | a.PlacementID = "myplacement" 21 | a.PlacementType = Banner 22 | a.Width = 400 23 | a.Height = 123 24 | _, err := dsp.Bid(context.Background(), a) 25 | if have, want := err, ErrNoBid; have != want { 26 | t.Errorf("have %v, want %v", have, want) 27 | } 28 | } 29 | 30 | func TestDSP(t *testing.T) { 31 | dsp, s := RunDSP("dsp", "My Second DSP", dsplib.Campaign{ 32 | Width: 400, Height: 500, BidCPM: 0.42, 33 | }) 34 | defer s.Close() 35 | 36 | a := NewAuction() 37 | a.UserAgent = "chromium 4.5.6" 38 | a.IP = "5.6.7.8" 39 | a.PlacementID = "myplacement" 40 | a.PlacementType = Banner 41 | a.Width = 400 42 | a.Height = 500 43 | bids, err := dsp.Bid(context.Background(), a) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if have, want := len(bids), 1; have != want { 48 | t.Fatalf("have %v, want %v", have, want) 49 | } 50 | bid := bids[0] 51 | if have, want := bid.PriceCPM, 0.42; have != want { 52 | t.Errorf("have %v, want %v", have, want) 53 | } 54 | } 55 | 56 | func TestDSPMultiple(t *testing.T) { 57 | dsp, s := RunDSP("dsp", "My Second DSP", 58 | dsplib.Campaign{ 59 | Width: 400, Height: 500, BidCPM: 0.42, 60 | }, 61 | dsplib.Campaign{ 62 | Width: 400, Height: 500, BidCPM: 0.52, 63 | }, 64 | dsplib.Campaign{ 65 | Width: 400, Height: 500, BidCPM: 0.22, 66 | }, 67 | ) 68 | defer s.Close() 69 | 70 | a := NewAuction() 71 | a.UserAgent = "chromium 4.5.6" 72 | a.IP = "5.6.7.8" 73 | a.PlacementID = "myplacement" 74 | a.PlacementType = Banner 75 | a.Width = 400 76 | a.Height = 500 77 | bids, err := dsp.Bid(context.Background(), a) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | if have, want := len(bids), 3; have != want { 82 | t.Fatalf("have %v, want %v", have, want) 83 | } 84 | bid := bids[0] 85 | if have, want := bid.PriceCPM, 0.42; have != want { 86 | t.Errorf("have %v, want %v", have, want) 87 | } 88 | } 89 | 90 | func TestVideo(t *testing.T) { 91 | dsp, s := RunDSP("dsp", "My Second DSP", dsplib.Campaign{ 92 | Width: 400, Height: 500, BidCPM: 0.42, 93 | Type: "video", 94 | VideoURL: "http://some.where/movie.mp4", 95 | }) 96 | defer s.Close() 97 | 98 | a := NewAuction() 99 | a.UserAgent = "chromium 4.5.6" 100 | a.IP = "5.6.7.8" 101 | a.PlacementID = "myplacement" 102 | a.PlacementType = Video 103 | a.Width = 400 104 | a.Height = 500 105 | bids, err := dsp.Bid(context.Background(), a) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | if have, want := len(bids), 1; have != want { 110 | t.Fatalf("have %v, want %v", have, want) 111 | } 112 | bid := bids[0] 113 | if have, want := bid.PriceCPM, 0.42; have != want { 114 | t.Errorf("have %v, want %v", have, want) 115 | } 116 | if want, have := "movie.mp4", bid.AdMarkup; !strings.Contains(have, want) { 117 | t.Fatalf("%q not found", want) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ssp/id.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var pool = "abcdefghijklmnopqrstuvwxqz0123456789" 9 | 10 | func init() { 11 | rand.Seed(time.Now().Unix()) 12 | } 13 | 14 | func RandomID(n int) string { 15 | res := make([]byte, n) 16 | for i := range res { 17 | res[i] = pool[rand.Intn(len(pool))] 18 | } 19 | return string(res) 20 | } 21 | -------------------------------------------------------------------------------- /ssp/placement.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | ) 7 | 8 | type Type string 9 | 10 | const ( 11 | Banner Type = "banner" 12 | Video Type = "video" 13 | ) 14 | 15 | type Placement struct { 16 | ID string `json:"id"` 17 | Name string `json:"name"` 18 | FloorCPM float64 `json:"floor_cpm"` 19 | Width int `json:"width"` 20 | Height int `json:"height"` 21 | Type Type `json:"type"` 22 | } 23 | 24 | func (p Placement) Code(base string) (string, error) { 25 | b := &bytes.Buffer{} 26 | args := struct { 27 | Base string 28 | Static string 29 | Placement Placement 30 | }{ 31 | Base: base, 32 | Static: base + "../../static/", 33 | Placement: p, 34 | } 35 | switch p.Type { 36 | case Video: 37 | if err := videoCode.Execute(b, args); err != nil { 38 | return "", err 39 | } 40 | default: 41 | if err := plainCode.Execute(b, args); err != nil { 42 | return "", err 43 | } 44 | } 45 | return b.String(), nil 46 | } 47 | 48 | func (p Placement) Iframe(au *Auction) (string, error) { 49 | b := &bytes.Buffer{} 50 | markup := template.HTML(au.AdMarkup) 51 | if err := plainIframe.Execute(b, markup); err != nil { 52 | return "", err 53 | } 54 | return b.String(), nil 55 | } 56 | 57 | var ( 58 | plainCode = template.Must(template.New("code").Parse(` 59 | 60 | `)) 61 | 62 | plainIframe = template.Must(template.New("embed").Parse(` 63 | 64 | 65 | {{.}} 66 | 74 | `)) 75 | 76 | videoCode = template.Must(template.New("code").Parse(` 77 | 78 | 79 | 82 | 89 | `)) 90 | ) 91 | -------------------------------------------------------------------------------- /ssp/testdsp.go: -------------------------------------------------------------------------------- 1 | package ssp 2 | 3 | import ( 4 | "github.com/alicebob/ssp/dsplib" 5 | ) 6 | 7 | // RunDSP returns a http server you need to Close() when done 8 | func RunDSP(id, name string, campaigns ...dsplib.Campaign) (DSP, *dsplib.DSP) { 9 | dsp := DSP{ 10 | ID: id, 11 | Name: name, 12 | } 13 | o := dsplib.NewDSP("localhost:0", campaigns) 14 | dsp.BidURL = o.BidURL 15 | return dsp, o 16 | } 17 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | // This file is automatically generated using github.com/mjibson/esc. 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "compress/gzip" 8 | "encoding/base64" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type _escLocalFS struct{} 18 | 19 | var _escLocal _escLocalFS 20 | 21 | type _escStaticFS struct{} 22 | 23 | var _escStatic _escStaticFS 24 | 25 | type _escDirectory struct { 26 | fs http.FileSystem 27 | name string 28 | } 29 | 30 | type _escFile struct { 31 | compressed string 32 | size int64 33 | modtime int64 34 | local string 35 | isDir bool 36 | 37 | once sync.Once 38 | data []byte 39 | name string 40 | } 41 | 42 | func (_escLocalFS) Open(name string) (http.File, error) { 43 | f, present := _escData[path.Clean(name)] 44 | if !present { 45 | return nil, os.ErrNotExist 46 | } 47 | return os.Open(f.local) 48 | } 49 | 50 | func (_escStaticFS) prepare(name string) (*_escFile, error) { 51 | f, present := _escData[path.Clean(name)] 52 | if !present { 53 | return nil, os.ErrNotExist 54 | } 55 | var err error 56 | f.once.Do(func() { 57 | f.name = path.Base(name) 58 | if f.size == 0 { 59 | return 60 | } 61 | var gr *gzip.Reader 62 | b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed)) 63 | gr, err = gzip.NewReader(b64) 64 | if err != nil { 65 | return 66 | } 67 | f.data, err = ioutil.ReadAll(gr) 68 | }) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return f, nil 73 | } 74 | 75 | func (fs _escStaticFS) Open(name string) (http.File, error) { 76 | f, err := fs.prepare(name) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return f.File() 81 | } 82 | 83 | func (dir _escDirectory) Open(name string) (http.File, error) { 84 | return dir.fs.Open(dir.name + name) 85 | } 86 | 87 | func (f *_escFile) File() (http.File, error) { 88 | type httpFile struct { 89 | *bytes.Reader 90 | *_escFile 91 | } 92 | return &httpFile{ 93 | Reader: bytes.NewReader(f.data), 94 | _escFile: f, 95 | }, nil 96 | } 97 | 98 | func (f *_escFile) Close() error { 99 | return nil 100 | } 101 | 102 | func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { 103 | return nil, nil 104 | } 105 | 106 | func (f *_escFile) Stat() (os.FileInfo, error) { 107 | return f, nil 108 | } 109 | 110 | func (f *_escFile) Name() string { 111 | return f.name 112 | } 113 | 114 | func (f *_escFile) Size() int64 { 115 | return f.size 116 | } 117 | 118 | func (f *_escFile) Mode() os.FileMode { 119 | return 0 120 | } 121 | 122 | func (f *_escFile) ModTime() time.Time { 123 | return time.Unix(f.modtime, 0) 124 | } 125 | 126 | func (f *_escFile) IsDir() bool { 127 | return f.isDir 128 | } 129 | 130 | func (f *_escFile) Sys() interface{} { 131 | return f 132 | } 133 | 134 | // FS returns a http.Filesystem for the embedded assets. If useLocal is true, 135 | // the filesystem's contents are instead used. 136 | func FS(useLocal bool) http.FileSystem { 137 | if useLocal { 138 | return _escLocal 139 | } 140 | return _escStatic 141 | } 142 | 143 | // Dir returns a http.Filesystem for the embedded assets on a given prefix dir. 144 | // If useLocal is true, the filesystem's contents are instead used. 145 | func Dir(useLocal bool, name string) http.FileSystem { 146 | if useLocal { 147 | return _escDirectory{fs: _escLocal, name: name} 148 | } 149 | return _escDirectory{fs: _escStatic, name: name} 150 | } 151 | 152 | // FSByte returns the named file from the embedded assets. If useLocal is 153 | // true, the filesystem's contents are instead used. 154 | func FSByte(useLocal bool, name string) ([]byte, error) { 155 | if useLocal { 156 | f, err := _escLocal.Open(name) 157 | if err != nil { 158 | return nil, err 159 | } 160 | b, err := ioutil.ReadAll(f) 161 | _ = f.Close() 162 | return b, err 163 | } 164 | f, err := _escStatic.prepare(name) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return f.data, nil 169 | } 170 | 171 | // FSMustByte is the same as FSByte, but panics if name is not present. 172 | func FSMustByte(useLocal bool, name string) []byte { 173 | b, err := FSByte(useLocal, name) 174 | if err != nil { 175 | panic(err) 176 | } 177 | return b 178 | } 179 | 180 | // FSString is the string version of FSByte. 181 | func FSString(useLocal bool, name string) (string, error) { 182 | b, err := FSByte(useLocal, name) 183 | return string(b), err 184 | } 185 | 186 | // FSMustString is the string version of FSMustByte. 187 | func FSMustString(useLocal bool, name string) string { 188 | return string(FSMustByte(useLocal, name)) 189 | } 190 | 191 | var _escData = map[string]*_escFile{ 192 | 193 | "/html5vast.css": { 194 | local: "static/html5vast.css", 195 | size: 295, 196 | modtime: 1497184022, 197 | compressed: ` 198 | H4sIAAAAAAAA/1zOMW7DMAwF0FkFeorOKpygWZipR2Es2iaqigJFJ2mL3L2QFGcIBw3vf1B07n05nC+K 199 | OZP+vb4457IUNpYEShGNz3R0rga3+tT2iLnmT208FYmr0bHxhYMtsBuGfO2wEM+Lwf4BJxy/ZpU1BT9K 200 | FIW3oU1P7zS16TRJMl/4l2C335Y0m/Cb4w98KmPsbHQ1j5HnBCMlI71/KRpIvWLgtcBhW2KS4eNxV8YQ 201 | OM2+6la5/QcAAP//SgrTmScBAAA= 202 | `, 203 | }, 204 | 205 | "/html5vast.js": { 206 | local: "static/html5vast.js", 207 | size: 15073, 208 | modtime: 1497526731, 209 | compressed: ` 210 | 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 258 | 3WXNqtT6bwAAAP//9YX6W+E6AAA= 259 | `, 260 | }, 261 | 262 | "/": { 263 | isDir: true, 264 | local: "static", 265 | }, 266 | } 267 | -------------------------------------------------------------------------------- /static/html5vast.css: -------------------------------------------------------------------------------- 1 | .h5vwrapper{ 2 | position:relative; 3 | } 4 | .h5vcaption{ 5 | position:absolute; 6 | width:100px; 7 | height:20px; 8 | background-color:#000000; 9 | color:#ffffff; 10 | font-size:12px; 11 | font-family:Arial; 12 | text-align:center; 13 | border-radius:5px; 14 | top:40px; 15 | padding-top:5px; 16 | } -------------------------------------------------------------------------------- /static/html5vast.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HTML5VAST - Play VAST 3.0 Ads on HTML5 Video 3 | * http://html5vast.com 4 | * Sadan Nasir 5 | * version 1.3 2015-04-15 6 | * Creative Commons Attribution-NonCommercial 4.0 International License 7 | * http://creativecommons.org/licenses/by-nc/4.0/ 8 | */ 9 | 10 | function html5vast(video_player_id, vast_url, options){ 11 | var video_player = document.getElementById(video_player_id); 12 | 13 | //Default options 14 | var html5vast_options = { 15 | 'media_type' : 'video/mp4', 16 | 'ad_caption': 'Advertisement' 17 | }; 18 | for(var key in options){ 19 | html5vast_options[key] = options[key]; 20 | } 21 | 22 | //Create Wrapper Div 23 | var wrapper_div = document.createElement('div'); 24 | wrapper_div.className = 'h5vwrapper'; 25 | wrapper_div.id = 'h5vwrapper_'+video_player_id; 26 | video_player.parentNode.insertBefore(wrapper_div,video_player); 27 | wrapper_div.appendChild(video_player); 28 | 29 | 30 | var obj_vast = h5vReadFile(vast_url,html5vast_options); 31 | //alert(obj_vast.media_file); 32 | h5vPreRoll(video_player_id,obj_vast, html5vast_options); 33 | 34 | 35 | } 36 | 37 | //Parse VAST XML 38 | function h5vReadFile(vast_url, options){ 39 | //Read XML file 40 | var xmlHttpReq; var xmlDoc; 41 | if (window.XMLHttpRequest){ 42 | xmlHttpReq=new XMLHttpRequest(); 43 | } 44 | else{ 45 | xmlHttpReq=new ActiveXObject("Microsoft.XMLHTTP"); 46 | } 47 | xmlHttpReq.open("GET",vast_url,false); 48 | xmlHttpReq.send(); 49 | xmlDoc=xmlHttpReq.responseXML; 50 | 51 | var obj_vast ={}; 52 | 53 | //Get impression tag 54 | var impression = xmlDoc.getElementsByTagName("Impression"); 55 | if(impression != null){ 56 | //obj_vast.impression_url = impression[0].childNodes[0].nodeValue; 57 | obj_vast.impression = impression; 58 | //alert(obj_vast.impression_url); 59 | } 60 | 61 | //Get Creative 62 | var creative = xmlDoc.getElementsByTagName("Creative"); 63 | var media_files; 64 | var tracking_events; 65 | for(var i=0;i=(obj_vast.duration-1))){ //End 324 | if(obj_vast.tracking_complete_tracked ==false){ 325 | if(obj_vast.tracking_complete != null){ 326 | var arrTrack = obj_vast.tracking_complete.split(" "); 327 | for(var i=0;i 98 | 3 ├s nil 99 | 2 |├earch\ *<2> 100 | 1 |└upport\ *<3> 101 | 2 ├blog\ *<4> 102 | 1 | └:post nil 103 | 1 | └\ *<5> 104 | 2 ├about-us\ *<6> 105 | 1 | └team\ *<7> 106 | 1 └contact\ *<8> 107 | ``` 108 | 109 | Every `*` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark), this works very well and efficient. 110 | 111 | Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree. 112 | 113 | For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways: 114 | 115 | 1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible. 116 | 2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right. 117 | 118 | ``` 119 | ├------------ 120 | ├--------- 121 | ├----- 122 | ├---- 123 | ├-- 124 | ├-- 125 | └- 126 | ``` 127 | 128 | ## Why doesn't this work with `http.Handler`? 129 | 130 | **It does!** The router itself implements the `http.Handler` interface. Moreover the router provides convenient [adapters for `http.Handler`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [`http.HandlerFunc`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s which allows them to be used as a [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route. The only disadvantage is, that no parameter values can be retrieved when a `http.Handler` or `http.HandlerFunc` is used, since there is no efficient way to pass the values with the existing function parameters. Therefore [`httprouter.Handle`](https://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) has a third function parameter. 131 | 132 | Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. 133 | 134 | ## Where can I find Middleware *X*? 135 | 136 | This package just provides a very efficient request router with a few extra features. The router is just a [`http.Handler`](https://golang.org/pkg/net/http/#Handler), you can chain any http.Handler compatible middleware before the router, for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers). Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy! 137 | 138 | Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter). 139 | 140 | ### Multi-domain / Sub-domains 141 | 142 | Here is a quick example: Does your server serve multiple domains / hosts? 143 | You want to use sub-domains? 144 | Define a router per host! 145 | 146 | ```go 147 | // We need an object that implements the http.Handler interface. 148 | // Therefore we need a type for which we implement the ServeHTTP method. 149 | // We just use a map here, in which we map host names (with port) to http.Handlers 150 | type HostSwitch map[string]http.Handler 151 | 152 | // Implement the ServerHTTP method on our new type 153 | func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { 154 | // Check if a http.Handler is registered for the given host. 155 | // If yes, use it to handle the request. 156 | if handler := hs[r.Host]; handler != nil { 157 | handler.ServeHTTP(w, r) 158 | } else { 159 | // Handle host names for wich no handler is registered 160 | http.Error(w, "Forbidden", 403) // Or Redirect? 161 | } 162 | } 163 | 164 | func main() { 165 | // Initialize a router as usual 166 | router := httprouter.New() 167 | router.GET("/", Index) 168 | router.GET("/hello/:name", Hello) 169 | 170 | // Make a new HostSwitch and insert the router (our http handler) 171 | // for example.com and port 12345 172 | hs := make(HostSwitch) 173 | hs["example.com:12345"] = router 174 | 175 | // Use the HostSwitch to listen and serve on port 12345 176 | log.Fatal(http.ListenAndServe(":12345", hs)) 177 | } 178 | ``` 179 | 180 | ### Basic Authentication 181 | 182 | Another quick example: Basic Authentication (RFC 2617) for handles: 183 | 184 | ```go 185 | package main 186 | 187 | import ( 188 | "fmt" 189 | "log" 190 | "net/http" 191 | 192 | "github.com/julienschmidt/httprouter" 193 | ) 194 | 195 | func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle { 196 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 197 | // Get the Basic Authentication credentials 198 | user, password, hasAuth := r.BasicAuth() 199 | 200 | if hasAuth && user == requiredUser && password == requiredPassword { 201 | // Delegate request to the given handle 202 | h(w, r, ps) 203 | } else { 204 | // Request Basic Authentication otherwise 205 | w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") 206 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 207 | } 208 | } 209 | } 210 | 211 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 212 | fmt.Fprint(w, "Not protected!\n") 213 | } 214 | 215 | func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 216 | fmt.Fprint(w, "Protected!\n") 217 | } 218 | 219 | func main() { 220 | user := "gordon" 221 | pass := "secret!" 222 | 223 | router := httprouter.New() 224 | router.GET("/", Index) 225 | router.GET("/protected/", BasicAuth(Protected, user, pass)) 226 | 227 | log.Fatal(http.ListenAndServe(":8080", router)) 228 | } 229 | ``` 230 | 231 | ## Chaining with the NotFound handler 232 | 233 | **NOTE: It might be required to set [`Router.HandleMethodNotAllowed`](https://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** 234 | 235 | You can use another [`http.Handler`](https://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [`Router.NotFound`](https://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining. 236 | 237 | ### Static files 238 | 239 | The `NotFound` handler can for example be used to serve static files from the root path `/` (like an `index.html` file along with other assets): 240 | 241 | ```go 242 | // Serve static files from the ./public directory 243 | router.NotFound = http.FileServer(http.Dir("public")) 244 | ``` 245 | 246 | But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`. 247 | 248 | ## Web Frameworks based on HttpRouter 249 | 250 | If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: 251 | 252 | * [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework 253 | * [api2go](https://github.com/manyminds/api2go): A JSON API Implementation for Go 254 | * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance 255 | * [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go 256 | * [goMiddlewareChain](https://github.com/TobiEiss/goMiddlewareChain): An express.js-like-middleware-chain 257 | * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine 258 | * [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow 259 | * [httpway](https://github.com/corneldamian/httpway): Simple middleware extension with context for httprouter and a server with gracefully shutdown support 260 | * [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context 261 | * [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba 262 | * [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang 263 | * [River](https://github.com/abiosoft/river): River is a simple and lightweight REST server 264 | * [Roxanna](https://github.com/iamthemuffinman/Roxanna): An amalgamation of httprouter, better logging, and hot reload 265 | * [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts 266 | * [xmux](https://github.com/rs/xmux): xmux is a httprouter fork on top of xhandler (net/context aware) 267 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 9 | // for p, eliminating . and .. elements. 10 | // 11 | // The following rules are applied iteratively until no further processing can 12 | // be done: 13 | // 1. Replace multiple slashes with a single slash. 14 | // 2. Eliminate each . path name element (the current directory). 15 | // 3. Eliminate each inner .. path name element (the parent directory) 16 | // along with the non-.. element that precedes it. 17 | // 4. Eliminate .. elements that begin a rooted path: 18 | // that is, replace "/.." by "/" at the beginning of a path. 19 | // 20 | // If the result of this process is an empty string, "/" is returned 21 | func CleanPath(p string) string { 22 | // Turn empty string into "/" 23 | if p == "" { 24 | return "/" 25 | } 26 | 27 | n := len(p) 28 | var buf []byte 29 | 30 | // Invariants: 31 | // reading from path; r is index of next byte to process. 32 | // writing to buf; w is index of next byte to write. 33 | 34 | // path must start with '/' 35 | r := 1 36 | w := 1 37 | 38 | if p[0] != '/' { 39 | r = 0 40 | buf = make([]byte, n+1) 41 | buf[0] = '/' 42 | } 43 | 44 | trailing := n > 2 && p[n-1] == '/' 45 | 46 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 47 | // gets completely inlined (bufApp). So in contrast to the path package this 48 | // loop has no expensive function calls (except 1x make) 49 | 50 | for r < n { 51 | switch { 52 | case p[r] == '/': 53 | // empty path element, trailing slash is added after the end 54 | r++ 55 | 56 | case p[r] == '.' && r+1 == n: 57 | trailing = true 58 | r++ 59 | 60 | case p[r] == '.' && p[r+1] == '/': 61 | // . element 62 | r++ 63 | 64 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 65 | // .. element: remove to last / 66 | r += 2 67 | 68 | if w > 1 { 69 | // can backtrack 70 | w-- 71 | 72 | if buf == nil { 73 | for w > 1 && p[w] != '/' { 74 | w-- 75 | } 76 | } else { 77 | for w > 1 && buf[w] != '/' { 78 | w-- 79 | } 80 | } 81 | } 82 | 83 | default: 84 | // real path element. 85 | // add slash if needed 86 | if w > 1 { 87 | bufApp(&buf, p, w, '/') 88 | w++ 89 | } 90 | 91 | // copy element 92 | for r < n && p[r] != '/' { 93 | bufApp(&buf, p, w, p[r]) 94 | w++ 95 | r++ 96 | } 97 | } 98 | } 99 | 100 | // re-append trailing slash 101 | if trailing && w > 1 { 102 | bufApp(&buf, p, w, '/') 103 | w++ 104 | } 105 | 106 | if buf == nil { 107 | return p[:w] 108 | } 109 | return string(buf[:w]) 110 | } 111 | 112 | // internal helper to lazily create a buffer if necessary 113 | func bufApp(buf *[]byte, s string, w int, c byte) { 114 | if *buf == nil { 115 | if s[w] == c { 116 | return 117 | } 118 | 119 | *buf = make([]byte, len(s)) 120 | copy(*buf, s[:w]) 121 | } 122 | (*buf)[w] = c 123 | } 124 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | // Package httprouter is a trie based high performance HTTP request router. 6 | // 7 | // A trivial example is: 8 | // 9 | // package main 10 | // 11 | // import ( 12 | // "fmt" 13 | // "github.com/julienschmidt/httprouter" 14 | // "net/http" 15 | // "log" 16 | // ) 17 | // 18 | // func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 19 | // fmt.Fprint(w, "Welcome!\n") 20 | // } 21 | // 22 | // func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 23 | // fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 24 | // } 25 | // 26 | // func main() { 27 | // router := httprouter.New() 28 | // router.GET("/", Index) 29 | // router.GET("/hello/:name", Hello) 30 | // 31 | // log.Fatal(http.ListenAndServe(":8080", router)) 32 | // } 33 | // 34 | // The router matches incoming requests by the request method and the path. 35 | // If a handle is registered for this path and method, the router delegates the 36 | // request to that function. 37 | // For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to 38 | // register handles, for all other methods router.Handle can be used. 39 | // 40 | // The registered path, against which the router matches incoming requests, can 41 | // contain two types of parameters: 42 | // Syntax Type 43 | // :name named parameter 44 | // *name catch-all parameter 45 | // 46 | // Named parameters are dynamic path segments. They match anything until the 47 | // next '/' or the path end: 48 | // Path: /blog/:category/:post 49 | // 50 | // Requests: 51 | // /blog/go/request-routers match: category="go", post="request-routers" 52 | // /blog/go/request-routers/ no match, but the router would redirect 53 | // /blog/go/ no match 54 | // /blog/go/request-routers/comments no match 55 | // 56 | // Catch-all parameters match anything until the path end, including the 57 | // directory index (the '/' before the catch-all). Since they match anything 58 | // until the end, catch-all parameters must always be the final path element. 59 | // Path: /files/*filepath 60 | // 61 | // Requests: 62 | // /files/ match: filepath="/" 63 | // /files/LICENSE match: filepath="/LICENSE" 64 | // /files/templates/article.html match: filepath="/templates/article.html" 65 | // /files no match, but the router would redirect 66 | // 67 | // The value of parameters is saved as a slice of the Param struct, consisting 68 | // each of a key and a value. The slice is passed to the Handle func as a third 69 | // parameter. 70 | // There are two ways to retrieve the value of a parameter: 71 | // // by the name of the parameter 72 | // user := ps.ByName("user") // defined by :user or *user 73 | // 74 | // // by the index of the parameter. This way you can also get the name (key) 75 | // thirdKey := ps[2].Key // the name of the 3rd parameter 76 | // thirdValue := ps[2].Value // the value of the 3rd parameter 77 | package httprouter 78 | 79 | import ( 80 | "net/http" 81 | ) 82 | 83 | // Handle is a function that can be registered to a route to handle HTTP 84 | // requests. Like http.HandlerFunc, but has a third parameter for the values of 85 | // wildcards (variables). 86 | type Handle func(http.ResponseWriter, *http.Request, Params) 87 | 88 | // Param is a single URL parameter, consisting of a key and a value. 89 | type Param struct { 90 | Key string 91 | Value string 92 | } 93 | 94 | // Params is a Param-slice, as returned by the router. 95 | // The slice is ordered, the first URL parameter is also the first slice value. 96 | // It is therefore safe to read values by the index. 97 | type Params []Param 98 | 99 | // ByName returns the value of the first Param which key matches the given name. 100 | // If no matching Param is found, an empty string is returned. 101 | func (ps Params) ByName(name string) string { 102 | for i := range ps { 103 | if ps[i].Key == name { 104 | return ps[i].Value 105 | } 106 | } 107 | return "" 108 | } 109 | 110 | // Router is a http.Handler which can be used to dispatch requests to different 111 | // handler functions via configurable routes 112 | type Router struct { 113 | trees map[string]*node 114 | 115 | // Enables automatic redirection if the current route can't be matched but a 116 | // handler for the path with (without) the trailing slash exists. 117 | // For example if /foo/ is requested but a route only exists for /foo, the 118 | // client is redirected to /foo with http status code 301 for GET requests 119 | // and 307 for all other request methods. 120 | RedirectTrailingSlash bool 121 | 122 | // If enabled, the router tries to fix the current request path, if no 123 | // handle is registered for it. 124 | // First superfluous path elements like ../ or // are removed. 125 | // Afterwards the router does a case-insensitive lookup of the cleaned path. 126 | // If a handle can be found for this route, the router makes a redirection 127 | // to the corrected path with status code 301 for GET requests and 307 for 128 | // all other request methods. 129 | // For example /FOO and /..//Foo could be redirected to /foo. 130 | // RedirectTrailingSlash is independent of this option. 131 | RedirectFixedPath bool 132 | 133 | // If enabled, the router checks if another method is allowed for the 134 | // current route, if the current request can not be routed. 135 | // If this is the case, the request is answered with 'Method Not Allowed' 136 | // and HTTP status code 405. 137 | // If no other Method is allowed, the request is delegated to the NotFound 138 | // handler. 139 | HandleMethodNotAllowed bool 140 | 141 | // If enabled, the router automatically replies to OPTIONS requests. 142 | // Custom OPTIONS handlers take priority over automatic replies. 143 | HandleOPTIONS bool 144 | 145 | // Configurable http.Handler which is called when no matching route is 146 | // found. If it is not set, http.NotFound is used. 147 | NotFound http.Handler 148 | 149 | // Configurable http.Handler which is called when a request 150 | // cannot be routed and HandleMethodNotAllowed is true. 151 | // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 152 | // The "Allow" header with allowed request methods is set before the handler 153 | // is called. 154 | MethodNotAllowed http.Handler 155 | 156 | // Function to handle panics recovered from http handlers. 157 | // It should be used to generate a error page and return the http error code 158 | // 500 (Internal Server Error). 159 | // The handler can be used to keep your server from crashing because of 160 | // unrecovered panics. 161 | PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 162 | } 163 | 164 | // Make sure the Router conforms with the http.Handler interface 165 | var _ http.Handler = New() 166 | 167 | // New returns a new initialized Router. 168 | // Path auto-correction, including trailing slashes, is enabled by default. 169 | func New() *Router { 170 | return &Router{ 171 | RedirectTrailingSlash: true, 172 | RedirectFixedPath: true, 173 | HandleMethodNotAllowed: true, 174 | HandleOPTIONS: true, 175 | } 176 | } 177 | 178 | // GET is a shortcut for router.Handle("GET", path, handle) 179 | func (r *Router) GET(path string, handle Handle) { 180 | r.Handle("GET", path, handle) 181 | } 182 | 183 | // HEAD is a shortcut for router.Handle("HEAD", path, handle) 184 | func (r *Router) HEAD(path string, handle Handle) { 185 | r.Handle("HEAD", path, handle) 186 | } 187 | 188 | // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) 189 | func (r *Router) OPTIONS(path string, handle Handle) { 190 | r.Handle("OPTIONS", path, handle) 191 | } 192 | 193 | // POST is a shortcut for router.Handle("POST", path, handle) 194 | func (r *Router) POST(path string, handle Handle) { 195 | r.Handle("POST", path, handle) 196 | } 197 | 198 | // PUT is a shortcut for router.Handle("PUT", path, handle) 199 | func (r *Router) PUT(path string, handle Handle) { 200 | r.Handle("PUT", path, handle) 201 | } 202 | 203 | // PATCH is a shortcut for router.Handle("PATCH", path, handle) 204 | func (r *Router) PATCH(path string, handle Handle) { 205 | r.Handle("PATCH", path, handle) 206 | } 207 | 208 | // DELETE is a shortcut for router.Handle("DELETE", path, handle) 209 | func (r *Router) DELETE(path string, handle Handle) { 210 | r.Handle("DELETE", path, handle) 211 | } 212 | 213 | // Handle registers a new request handle with the given path and method. 214 | // 215 | // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 216 | // functions can be used. 217 | // 218 | // This function is intended for bulk loading and to allow the usage of less 219 | // frequently used, non-standardized or custom methods (e.g. for internal 220 | // communication with a proxy). 221 | func (r *Router) Handle(method, path string, handle Handle) { 222 | if path[0] != '/' { 223 | panic("path must begin with '/' in path '" + path + "'") 224 | } 225 | 226 | if r.trees == nil { 227 | r.trees = make(map[string]*node) 228 | } 229 | 230 | root := r.trees[method] 231 | if root == nil { 232 | root = new(node) 233 | r.trees[method] = root 234 | } 235 | 236 | root.addRoute(path, handle) 237 | } 238 | 239 | // Handler is an adapter which allows the usage of an http.Handler as a 240 | // request handle. 241 | func (r *Router) Handler(method, path string, handler http.Handler) { 242 | r.Handle(method, path, 243 | func(w http.ResponseWriter, req *http.Request, _ Params) { 244 | handler.ServeHTTP(w, req) 245 | }, 246 | ) 247 | } 248 | 249 | // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a 250 | // request handle. 251 | func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { 252 | r.Handler(method, path, handler) 253 | } 254 | 255 | // ServeFiles serves files from the given file system root. 256 | // The path must end with "/*filepath", files are then served from the local 257 | // path /defined/root/dir/*filepath. 258 | // For example if root is "/etc" and *filepath is "passwd", the local file 259 | // "/etc/passwd" would be served. 260 | // Internally a http.FileServer is used, therefore http.NotFound is used instead 261 | // of the Router's NotFound handler. 262 | // To use the operating system's file system implementation, 263 | // use http.Dir: 264 | // router.ServeFiles("/src/*filepath", http.Dir("/var/www")) 265 | func (r *Router) ServeFiles(path string, root http.FileSystem) { 266 | if len(path) < 10 || path[len(path)-10:] != "/*filepath" { 267 | panic("path must end with /*filepath in path '" + path + "'") 268 | } 269 | 270 | fileServer := http.FileServer(root) 271 | 272 | r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { 273 | req.URL.Path = ps.ByName("filepath") 274 | fileServer.ServeHTTP(w, req) 275 | }) 276 | } 277 | 278 | func (r *Router) recv(w http.ResponseWriter, req *http.Request) { 279 | if rcv := recover(); rcv != nil { 280 | r.PanicHandler(w, req, rcv) 281 | } 282 | } 283 | 284 | // Lookup allows the manual lookup of a method + path combo. 285 | // This is e.g. useful to build a framework around this router. 286 | // If the path was found, it returns the handle function and the path parameter 287 | // values. Otherwise the third return value indicates whether a redirection to 288 | // the same path with an extra / without the trailing slash should be performed. 289 | func (r *Router) Lookup(method, path string) (Handle, Params, bool) { 290 | if root := r.trees[method]; root != nil { 291 | return root.getValue(path) 292 | } 293 | return nil, nil, false 294 | } 295 | 296 | func (r *Router) allowed(path, reqMethod string) (allow string) { 297 | if path == "*" { // server-wide 298 | for method := range r.trees { 299 | if method == "OPTIONS" { 300 | continue 301 | } 302 | 303 | // add request method to list of allowed methods 304 | if len(allow) == 0 { 305 | allow = method 306 | } else { 307 | allow += ", " + method 308 | } 309 | } 310 | } else { // specific path 311 | for method := range r.trees { 312 | // Skip the requested method - we already tried this one 313 | if method == reqMethod || method == "OPTIONS" { 314 | continue 315 | } 316 | 317 | handle, _, _ := r.trees[method].getValue(path) 318 | if handle != nil { 319 | // add request method to list of allowed methods 320 | if len(allow) == 0 { 321 | allow = method 322 | } else { 323 | allow += ", " + method 324 | } 325 | } 326 | } 327 | } 328 | if len(allow) > 0 { 329 | allow += ", OPTIONS" 330 | } 331 | return 332 | } 333 | 334 | // ServeHTTP makes the router implement the http.Handler interface. 335 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 336 | if r.PanicHandler != nil { 337 | defer r.recv(w, req) 338 | } 339 | 340 | path := req.URL.Path 341 | 342 | if root := r.trees[req.Method]; root != nil { 343 | if handle, ps, tsr := root.getValue(path); handle != nil { 344 | handle(w, req, ps) 345 | return 346 | } else if req.Method != "CONNECT" && path != "/" { 347 | code := 301 // Permanent redirect, request with GET method 348 | if req.Method != "GET" { 349 | // Temporary redirect, request with same method 350 | // As of Go 1.3, Go does not support status code 308. 351 | code = 307 352 | } 353 | 354 | if tsr && r.RedirectTrailingSlash { 355 | if len(path) > 1 && path[len(path)-1] == '/' { 356 | req.URL.Path = path[:len(path)-1] 357 | } else { 358 | req.URL.Path = path + "/" 359 | } 360 | http.Redirect(w, req, req.URL.String(), code) 361 | return 362 | } 363 | 364 | // Try to fix the request path 365 | if r.RedirectFixedPath { 366 | fixedPath, found := root.findCaseInsensitivePath( 367 | CleanPath(path), 368 | r.RedirectTrailingSlash, 369 | ) 370 | if found { 371 | req.URL.Path = string(fixedPath) 372 | http.Redirect(w, req, req.URL.String(), code) 373 | return 374 | } 375 | } 376 | } 377 | } 378 | 379 | if req.Method == "OPTIONS" { 380 | // Handle OPTIONS requests 381 | if r.HandleOPTIONS { 382 | if allow := r.allowed(path, req.Method); len(allow) > 0 { 383 | w.Header().Set("Allow", allow) 384 | return 385 | } 386 | } 387 | } else { 388 | // Handle 405 389 | if r.HandleMethodNotAllowed { 390 | if allow := r.allowed(path, req.Method); len(allow) > 0 { 391 | w.Header().Set("Allow", allow) 392 | if r.MethodNotAllowed != nil { 393 | r.MethodNotAllowed.ServeHTTP(w, req) 394 | } else { 395 | http.Error(w, 396 | http.StatusText(http.StatusMethodNotAllowed), 397 | http.StatusMethodNotAllowed, 398 | ) 399 | } 400 | return 401 | } 402 | } 403 | } 404 | 405 | // Handle 404 406 | if r.NotFound != nil { 407 | r.NotFound.ServeHTTP(w, req) 408 | } else { 409 | http.NotFound(w, req) 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/tree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | func min(a, b int) int { 14 | if a <= b { 15 | return a 16 | } 17 | return b 18 | } 19 | 20 | func countParams(path string) uint8 { 21 | var n uint 22 | for i := 0; i < len(path); i++ { 23 | if path[i] != ':' && path[i] != '*' { 24 | continue 25 | } 26 | n++ 27 | } 28 | if n >= 255 { 29 | return 255 30 | } 31 | return uint8(n) 32 | } 33 | 34 | type nodeType uint8 35 | 36 | const ( 37 | static nodeType = iota // default 38 | root 39 | param 40 | catchAll 41 | ) 42 | 43 | type node struct { 44 | path string 45 | wildChild bool 46 | nType nodeType 47 | maxParams uint8 48 | indices string 49 | children []*node 50 | handle Handle 51 | priority uint32 52 | } 53 | 54 | // increments priority of the given child and reorders if necessary 55 | func (n *node) incrementChildPrio(pos int) int { 56 | n.children[pos].priority++ 57 | prio := n.children[pos].priority 58 | 59 | // adjust position (move to front) 60 | newPos := pos 61 | for newPos > 0 && n.children[newPos-1].priority < prio { 62 | // swap node positions 63 | n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] 64 | 65 | newPos-- 66 | } 67 | 68 | // build new index char string 69 | if newPos != pos { 70 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty 71 | n.indices[pos:pos+1] + // the index char we move 72 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' 73 | } 74 | 75 | return newPos 76 | } 77 | 78 | // addRoute adds a node with the given handle to the path. 79 | // Not concurrency-safe! 80 | func (n *node) addRoute(path string, handle Handle) { 81 | fullPath := path 82 | n.priority++ 83 | numParams := countParams(path) 84 | 85 | // non-empty tree 86 | if len(n.path) > 0 || len(n.children) > 0 { 87 | walk: 88 | for { 89 | // Update maxParams of the current node 90 | if numParams > n.maxParams { 91 | n.maxParams = numParams 92 | } 93 | 94 | // Find the longest common prefix. 95 | // This also implies that the common prefix contains no ':' or '*' 96 | // since the existing key can't contain those chars. 97 | i := 0 98 | max := min(len(path), len(n.path)) 99 | for i < max && path[i] == n.path[i] { 100 | i++ 101 | } 102 | 103 | // Split edge 104 | if i < len(n.path) { 105 | child := node{ 106 | path: n.path[i:], 107 | wildChild: n.wildChild, 108 | nType: static, 109 | indices: n.indices, 110 | children: n.children, 111 | handle: n.handle, 112 | priority: n.priority - 1, 113 | } 114 | 115 | // Update maxParams (max of all children) 116 | for i := range child.children { 117 | if child.children[i].maxParams > child.maxParams { 118 | child.maxParams = child.children[i].maxParams 119 | } 120 | } 121 | 122 | n.children = []*node{&child} 123 | // []byte for proper unicode char conversion, see #65 124 | n.indices = string([]byte{n.path[i]}) 125 | n.path = path[:i] 126 | n.handle = nil 127 | n.wildChild = false 128 | } 129 | 130 | // Make new node a child of this node 131 | if i < len(path) { 132 | path = path[i:] 133 | 134 | if n.wildChild { 135 | n = n.children[0] 136 | n.priority++ 137 | 138 | // Update maxParams of the child node 139 | if numParams > n.maxParams { 140 | n.maxParams = numParams 141 | } 142 | numParams-- 143 | 144 | // Check if the wildcard matches 145 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] && 146 | // Check for longer wildcard, e.g. :name and :names 147 | (len(n.path) >= len(path) || path[len(n.path)] == '/') { 148 | continue walk 149 | } else { 150 | // Wildcard conflict 151 | var pathSeg string 152 | if n.nType == catchAll { 153 | pathSeg = path 154 | } else { 155 | pathSeg = strings.SplitN(path, "/", 2)[0] 156 | } 157 | prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path 158 | panic("'" + pathSeg + 159 | "' in new path '" + fullPath + 160 | "' conflicts with existing wildcard '" + n.path + 161 | "' in existing prefix '" + prefix + 162 | "'") 163 | } 164 | } 165 | 166 | c := path[0] 167 | 168 | // slash after param 169 | if n.nType == param && c == '/' && len(n.children) == 1 { 170 | n = n.children[0] 171 | n.priority++ 172 | continue walk 173 | } 174 | 175 | // Check if a child with the next path byte exists 176 | for i := 0; i < len(n.indices); i++ { 177 | if c == n.indices[i] { 178 | i = n.incrementChildPrio(i) 179 | n = n.children[i] 180 | continue walk 181 | } 182 | } 183 | 184 | // Otherwise insert it 185 | if c != ':' && c != '*' { 186 | // []byte for proper unicode char conversion, see #65 187 | n.indices += string([]byte{c}) 188 | child := &node{ 189 | maxParams: numParams, 190 | } 191 | n.children = append(n.children, child) 192 | n.incrementChildPrio(len(n.indices) - 1) 193 | n = child 194 | } 195 | n.insertChild(numParams, path, fullPath, handle) 196 | return 197 | 198 | } else if i == len(path) { // Make node a (in-path) leaf 199 | if n.handle != nil { 200 | panic("a handle is already registered for path '" + fullPath + "'") 201 | } 202 | n.handle = handle 203 | } 204 | return 205 | } 206 | } else { // Empty tree 207 | n.insertChild(numParams, path, fullPath, handle) 208 | n.nType = root 209 | } 210 | } 211 | 212 | func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) { 213 | var offset int // already handled bytes of the path 214 | 215 | // find prefix until first wildcard (beginning with ':'' or '*'') 216 | for i, max := 0, len(path); numParams > 0; i++ { 217 | c := path[i] 218 | if c != ':' && c != '*' { 219 | continue 220 | } 221 | 222 | // find wildcard end (either '/' or path end) 223 | end := i + 1 224 | for end < max && path[end] != '/' { 225 | switch path[end] { 226 | // the wildcard name must not contain ':' and '*' 227 | case ':', '*': 228 | panic("only one wildcard per path segment is allowed, has: '" + 229 | path[i:] + "' in path '" + fullPath + "'") 230 | default: 231 | end++ 232 | } 233 | } 234 | 235 | // check if this Node existing children which would be 236 | // unreachable if we insert the wildcard here 237 | if len(n.children) > 0 { 238 | panic("wildcard route '" + path[i:end] + 239 | "' conflicts with existing children in path '" + fullPath + "'") 240 | } 241 | 242 | // check if the wildcard has a name 243 | if end-i < 2 { 244 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 245 | } 246 | 247 | if c == ':' { // param 248 | // split path at the beginning of the wildcard 249 | if i > 0 { 250 | n.path = path[offset:i] 251 | offset = i 252 | } 253 | 254 | child := &node{ 255 | nType: param, 256 | maxParams: numParams, 257 | } 258 | n.children = []*node{child} 259 | n.wildChild = true 260 | n = child 261 | n.priority++ 262 | numParams-- 263 | 264 | // if the path doesn't end with the wildcard, then there 265 | // will be another non-wildcard subpath starting with '/' 266 | if end < max { 267 | n.path = path[offset:end] 268 | offset = end 269 | 270 | child := &node{ 271 | maxParams: numParams, 272 | priority: 1, 273 | } 274 | n.children = []*node{child} 275 | n = child 276 | } 277 | 278 | } else { // catchAll 279 | if end != max || numParams > 1 { 280 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 281 | } 282 | 283 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 284 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 285 | } 286 | 287 | // currently fixed width 1 for '/' 288 | i-- 289 | if path[i] != '/' { 290 | panic("no / before catch-all in path '" + fullPath + "'") 291 | } 292 | 293 | n.path = path[offset:i] 294 | 295 | // first node: catchAll node with empty path 296 | child := &node{ 297 | wildChild: true, 298 | nType: catchAll, 299 | maxParams: 1, 300 | } 301 | n.children = []*node{child} 302 | n.indices = string(path[i]) 303 | n = child 304 | n.priority++ 305 | 306 | // second node: node holding the variable 307 | child = &node{ 308 | path: path[i:], 309 | nType: catchAll, 310 | maxParams: 1, 311 | handle: handle, 312 | priority: 1, 313 | } 314 | n.children = []*node{child} 315 | 316 | return 317 | } 318 | } 319 | 320 | // insert remaining path part and handle to the leaf 321 | n.path = path[offset:] 322 | n.handle = handle 323 | } 324 | 325 | // Returns the handle registered with the given path (key). The values of 326 | // wildcards are saved to a map. 327 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is 328 | // made if a handle exists with an extra (without the) trailing slash for the 329 | // given path. 330 | func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) { 331 | walk: // outer loop for walking the tree 332 | for { 333 | if len(path) > len(n.path) { 334 | if path[:len(n.path)] == n.path { 335 | path = path[len(n.path):] 336 | // If this node does not have a wildcard (param or catchAll) 337 | // child, we can just look up the next child node and continue 338 | // to walk down the tree 339 | if !n.wildChild { 340 | c := path[0] 341 | for i := 0; i < len(n.indices); i++ { 342 | if c == n.indices[i] { 343 | n = n.children[i] 344 | continue walk 345 | } 346 | } 347 | 348 | // Nothing found. 349 | // We can recommend to redirect to the same URL without a 350 | // trailing slash if a leaf exists for that path. 351 | tsr = (path == "/" && n.handle != nil) 352 | return 353 | 354 | } 355 | 356 | // handle wildcard child 357 | n = n.children[0] 358 | switch n.nType { 359 | case param: 360 | // find param end (either '/' or path end) 361 | end := 0 362 | for end < len(path) && path[end] != '/' { 363 | end++ 364 | } 365 | 366 | // save param value 367 | if p == nil { 368 | // lazy allocation 369 | p = make(Params, 0, n.maxParams) 370 | } 371 | i := len(p) 372 | p = p[:i+1] // expand slice within preallocated capacity 373 | p[i].Key = n.path[1:] 374 | p[i].Value = path[:end] 375 | 376 | // we need to go deeper! 377 | if end < len(path) { 378 | if len(n.children) > 0 { 379 | path = path[end:] 380 | n = n.children[0] 381 | continue walk 382 | } 383 | 384 | // ... but we can't 385 | tsr = (len(path) == end+1) 386 | return 387 | } 388 | 389 | if handle = n.handle; handle != nil { 390 | return 391 | } else if len(n.children) == 1 { 392 | // No handle found. Check if a handle for this path + a 393 | // trailing slash exists for TSR recommendation 394 | n = n.children[0] 395 | tsr = (n.path == "/" && n.handle != nil) 396 | } 397 | 398 | return 399 | 400 | case catchAll: 401 | // save param value 402 | if p == nil { 403 | // lazy allocation 404 | p = make(Params, 0, n.maxParams) 405 | } 406 | i := len(p) 407 | p = p[:i+1] // expand slice within preallocated capacity 408 | p[i].Key = n.path[2:] 409 | p[i].Value = path 410 | 411 | handle = n.handle 412 | return 413 | 414 | default: 415 | panic("invalid node type") 416 | } 417 | } 418 | } else if path == n.path { 419 | // We should have reached the node containing the handle. 420 | // Check if this node has a handle registered. 421 | if handle = n.handle; handle != nil { 422 | return 423 | } 424 | 425 | if path == "/" && n.wildChild && n.nType != root { 426 | tsr = true 427 | return 428 | } 429 | 430 | // No handle found. Check if a handle for this path + a 431 | // trailing slash exists for trailing slash recommendation 432 | for i := 0; i < len(n.indices); i++ { 433 | if n.indices[i] == '/' { 434 | n = n.children[i] 435 | tsr = (len(n.path) == 1 && n.handle != nil) || 436 | (n.nType == catchAll && n.children[0].handle != nil) 437 | return 438 | } 439 | } 440 | 441 | return 442 | } 443 | 444 | // Nothing found. We can recommend to redirect to the same URL with an 445 | // extra trailing slash if a leaf exists for that path 446 | tsr = (path == "/") || 447 | (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && 448 | path == n.path[:len(n.path)-1] && n.handle != nil) 449 | return 450 | } 451 | } 452 | 453 | // Makes a case-insensitive lookup of the given path and tries to find a handler. 454 | // It can optionally also fix trailing slashes. 455 | // It returns the case-corrected path and a bool indicating whether the lookup 456 | // was successful. 457 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { 458 | return n.findCaseInsensitivePathRec( 459 | path, 460 | strings.ToLower(path), 461 | make([]byte, 0, len(path)+1), // preallocate enough memory for new path 462 | [4]byte{}, // empty rune buffer 463 | fixTrailingSlash, 464 | ) 465 | } 466 | 467 | // shift bytes in array by n bytes left 468 | func shiftNRuneBytes(rb [4]byte, n int) [4]byte { 469 | switch n { 470 | case 0: 471 | return rb 472 | case 1: 473 | return [4]byte{rb[1], rb[2], rb[3], 0} 474 | case 2: 475 | return [4]byte{rb[2], rb[3]} 476 | case 3: 477 | return [4]byte{rb[3]} 478 | default: 479 | return [4]byte{} 480 | } 481 | } 482 | 483 | // recursive case-insensitive lookup function used by n.findCaseInsensitivePath 484 | func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { 485 | loNPath := strings.ToLower(n.path) 486 | 487 | walk: // outer loop for walking the tree 488 | for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) { 489 | // add common path to result 490 | ciPath = append(ciPath, n.path...) 491 | 492 | if path = path[len(n.path):]; len(path) > 0 { 493 | loOld := loPath 494 | loPath = loPath[len(loNPath):] 495 | 496 | // If this node does not have a wildcard (param or catchAll) child, 497 | // we can just look up the next child node and continue to walk down 498 | // the tree 499 | if !n.wildChild { 500 | // skip rune bytes already processed 501 | rb = shiftNRuneBytes(rb, len(loNPath)) 502 | 503 | if rb[0] != 0 { 504 | // old rune not finished 505 | for i := 0; i < len(n.indices); i++ { 506 | if n.indices[i] == rb[0] { 507 | // continue with child node 508 | n = n.children[i] 509 | loNPath = strings.ToLower(n.path) 510 | continue walk 511 | } 512 | } 513 | } else { 514 | // process a new rune 515 | var rv rune 516 | 517 | // find rune start 518 | // runes are up to 4 byte long, 519 | // -4 would definitely be another rune 520 | var off int 521 | for max := min(len(loNPath), 3); off < max; off++ { 522 | if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) { 523 | // read rune from cached lowercase path 524 | rv, _ = utf8.DecodeRuneInString(loOld[i:]) 525 | break 526 | } 527 | } 528 | 529 | // calculate lowercase bytes of current rune 530 | utf8.EncodeRune(rb[:], rv) 531 | // skipp already processed bytes 532 | rb = shiftNRuneBytes(rb, off) 533 | 534 | for i := 0; i < len(n.indices); i++ { 535 | // lowercase matches 536 | if n.indices[i] == rb[0] { 537 | // must use a recursive approach since both the 538 | // uppercase byte and the lowercase byte might exist 539 | // as an index 540 | if out, found := n.children[i].findCaseInsensitivePathRec( 541 | path, loPath, ciPath, rb, fixTrailingSlash, 542 | ); found { 543 | return out, true 544 | } 545 | break 546 | } 547 | } 548 | 549 | // same for uppercase rune, if it differs 550 | if up := unicode.ToUpper(rv); up != rv { 551 | utf8.EncodeRune(rb[:], up) 552 | rb = shiftNRuneBytes(rb, off) 553 | 554 | for i := 0; i < len(n.indices); i++ { 555 | // uppercase matches 556 | if n.indices[i] == rb[0] { 557 | // continue with child node 558 | n = n.children[i] 559 | loNPath = strings.ToLower(n.path) 560 | continue walk 561 | } 562 | } 563 | } 564 | } 565 | 566 | // Nothing found. We can recommend to redirect to the same URL 567 | // without a trailing slash if a leaf exists for that path 568 | return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil) 569 | } 570 | 571 | n = n.children[0] 572 | switch n.nType { 573 | case param: 574 | // find param end (either '/' or path end) 575 | k := 0 576 | for k < len(path) && path[k] != '/' { 577 | k++ 578 | } 579 | 580 | // add param value to case insensitive path 581 | ciPath = append(ciPath, path[:k]...) 582 | 583 | // we need to go deeper! 584 | if k < len(path) { 585 | if len(n.children) > 0 { 586 | // continue with child node 587 | n = n.children[0] 588 | loNPath = strings.ToLower(n.path) 589 | loPath = loPath[k:] 590 | path = path[k:] 591 | continue 592 | } 593 | 594 | // ... but we can't 595 | if fixTrailingSlash && len(path) == k+1 { 596 | return ciPath, true 597 | } 598 | return ciPath, false 599 | } 600 | 601 | if n.handle != nil { 602 | return ciPath, true 603 | } else if fixTrailingSlash && len(n.children) == 1 { 604 | // No handle found. Check if a handle for this path + a 605 | // trailing slash exists 606 | n = n.children[0] 607 | if n.path == "/" && n.handle != nil { 608 | return append(ciPath, '/'), true 609 | } 610 | } 611 | return ciPath, false 612 | 613 | case catchAll: 614 | return append(ciPath, path...), true 615 | 616 | default: 617 | panic("invalid node type") 618 | } 619 | } else { 620 | // We should have reached the node containing the handle. 621 | // Check if this node has a handle registered. 622 | if n.handle != nil { 623 | return ciPath, true 624 | } 625 | 626 | // No handle found. 627 | // Try to fix the path by adding a trailing slash 628 | if fixTrailingSlash { 629 | for i := 0; i < len(n.indices); i++ { 630 | if n.indices[i] == '/' { 631 | n = n.children[i] 632 | if (len(n.path) == 1 && n.handle != nil) || 633 | (n.nType == catchAll && n.children[0].handle != nil) { 634 | return append(ciPath, '/'), true 635 | } 636 | return ciPath, false 637 | } 638 | } 639 | } 640 | return ciPath, false 641 | } 642 | } 643 | 644 | // Nothing found. 645 | // Try to fix the path by adding / removing a trailing slash 646 | if fixTrailingSlash { 647 | if path == "/" { 648 | return ciPath, true 649 | } 650 | if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' && 651 | loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil { 652 | return append(ciPath, n.path...), true 653 | } 654 | } 655 | return ciPath, false 656 | } 657 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "gKyBj05YkfuLFruAyPZ4KV9nFp8=", 7 | "path": "github.com/julienschmidt/httprouter", 8 | "revision": "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e", 9 | "revisionTime": "2017-04-30T22:20:11Z" 10 | } 11 | ], 12 | "rootPath": "github.com/alicebob/ssp" 13 | } 14 | --------------------------------------------------------------------------------