Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make JSON schema available for verification under https:// URIs #739

Merged
merged 2 commits into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 38 additions & 36 deletions schema/fs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

126 changes: 126 additions & 0 deletions schema/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2018 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package schema

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"

"github.com/xeipuuv/gojsonreference"
"github.com/xeipuuv/gojsonschema"
)

// fsLoaderFactory implements gojsonschema.JSONLoaderFactory by reading files under the specified namespaces from the root of fs.
type fsLoaderFactory struct {
namespaces []string
fs http.FileSystem
}

// newFSLoaderFactory returns a fsLoaderFactory reading files under the specified namespaces from the root of fs.
func newFSLoaderFactory(namespaces []string, fs http.FileSystem) *fsLoaderFactory {
return &fsLoaderFactory{
namespaces: namespaces,
fs: fs,
}
}

func (factory *fsLoaderFactory) New(source string) gojsonschema.JSONLoader {
return &fsLoader{
factory: factory,
source: source,
}
}

// refContents returns the contents of ref, if available in fsLoaderFactory.
func (factory *fsLoaderFactory) refContents(ref gojsonreference.JsonReference) ([]byte, error) {
refStr := ref.String()
path := ""
for _, ns := range factory.namespaces {
if strings.HasPrefix(refStr, ns) {
path = "/" + strings.TrimPrefix(refStr, ns)
break
}
}
if path == "" {
return nil, fmt.Errorf("Schema reference %#v unexpectedly not available in fsLoaderFactory with namespaces %#v", path, factory.namespaces)
}

f, err := factory.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

return ioutil.ReadAll(f)
}

// fsLoader implements gojsonschema.JSONLoader by reading the document named by source from a fsLoaderFactory.
type fsLoader struct {
factory *fsLoaderFactory
source string
}

// JsonSource implements gojsonschema.JSONLoader.JsonSource. The "Json" capitalization needs to be maintained to conform to the interface.
func (l *fsLoader) JsonSource() interface{} { // nolint: golint
return l.source
}

func (l *fsLoader) LoadJSON() (interface{}, error) {
// Based on gojsonschema.jsonReferenceLoader.LoadJSON.
reference, err := gojsonreference.NewJsonReference(l.source)
if err != nil {
return nil, err
}

refToURL := reference
refToURL.GetUrl().Fragment = ""

body, err := l.factory.refContents(refToURL)
if err != nil {
return nil, err
}

return decodeJSONUsingNumber(bytes.NewReader(body))
}

// decodeJSONUsingNumber returns JSON parsed from an io.Reader
func decodeJSONUsingNumber(r io.Reader) (interface{}, error) {
// Copied from gojsonschema.
var document interface{}

decoder := json.NewDecoder(r)
decoder.UseNumber()

err := decoder.Decode(&document)
if err != nil {
return nil, err
}

return document, nil
}

// JsonReference implements gojsonschema.JSONLoader.JsonReference. The "Json" capitalization needs to be maintained to conform to the interface.
func (l *fsLoader) JsonReference() (gojsonreference.JsonReference, error) { // nolint: golint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONReference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the comment says,

The "Json" capitalization needs to be maintained to conform to the interface.

return gojsonreference.NewJsonReference(l.JsonSource().(string))
}

func (l *fsLoader) LoaderFactory() gojsonschema.JSONLoaderFactory {
return l.factory
}
35 changes: 29 additions & 6 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,36 @@ var (
// having the OCI JSON schema files in root "/".
fs = _escFS(false)

// specs maps OCI schema media types to schema files.
// schemaNamespaces is a set of URI prefixes which are treated as containing the schema files of fs.
// This is necessary because *.json schema files in this directory use "id" and "$ref" attributes which evaluate to such URIs, e.g.
// ./image-manifest-schema.json URI contains
// "id": "https://opencontainers.org/schema/image/manifest",
// and
// "$ref": "content-descriptor.json"
// which evaluates as a link to https://opencontainers.org/schema/image/content-descriptor.json .
//
// To support such links without accessing the network (and trying to load content which is not hosted at these URIs),
// fsLoaderFactory accepts any URI starting with one of the schemaNamespaces below,
// and uses _escFS to load them from the root of its in-memory filesystem tree.
//
// (Note that this must contain subdirectories before its parent directories for fsLoaderFactory.refContents to work.)
schemaNamespaces = []string{
"https://opencontainers.org/schema/image/descriptor/",
"https://opencontainers.org/schema/image/index/",
"https://opencontainers.org/schema/image/manifest/",
"https://opencontainers.org/schema/image/",
"https://opencontainers.org/schema/",
}

// specs maps OCI schema media types to schema URIs.
// These URIs are expected to be used only by fsLoaderFactory (which trims schemaNamespaces defined above)
// and should never cause a network access.
specs = map[Validator]string{
ValidatorMediaTypeDescriptor: "content-descriptor.json",
ValidatorMediaTypeLayoutHeader: "image-layout-schema.json",
ValidatorMediaTypeManifest: "image-manifest-schema.json",
ValidatorMediaTypeImageIndex: "image-index-schema.json",
ValidatorMediaTypeImageConfig: "config-schema.json",
ValidatorMediaTypeDescriptor: "https://opencontainers.org/schema/content-descriptor.json",
ValidatorMediaTypeLayoutHeader: "https://opencontainers.org/schema/image/image-layout-schema.json",
ValidatorMediaTypeManifest: "https://opencontainers.org/schema/image/image-manifest-schema.json",
ValidatorMediaTypeImageIndex: "https://opencontainers.org/schema/image/image-index-schema.json",
ValidatorMediaTypeImageConfig: "https://opencontainers.org/schema/image/config-schema.json",
}
)

Expand Down
2 changes: 1 addition & 1 deletion schema/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (v Validator) Validate(src io.Reader) error {
}
}

sl := gojsonschema.NewReferenceLoaderFileSystem("file:///"+specs[v], fs)
sl := newFSLoaderFactory(schemaNamespaces, fs).New(specs[v])
ml := gojsonschema.NewStringLoader(string(buf))

result, err := gojsonschema.Validate(sl, ml)
Expand Down