Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

1,617 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Via

An app that proxies web pages and PDF files and injects the Hypothesis client so you can annotate them.

Setting up Your Via Development Environment

First you'll need to install:

  • Git. On Ubuntu: sudo apt install git, on macOS: brew install git.
  • GNU Make. This is probably already installed, run make --version to check.
  • pyenv. Follow the instructions in pyenv's README to install it. The Homebrew method works best on macOS. The Basic GitHub Checkout method works best on Ubuntu. You don't need to set up pyenv's shell integration ("shims"), you can use pyenv without shims.
  • Docker Desktop. On Ubuntu follow Install on Ubuntu. On macOS follow Install on Mac.
  • Node and npm. On Ubuntu: sudo snap install --classic node. On macOS: brew install node.
  • Yarn: sudo npm install -g yarn.

Then to set up your development environment:

git clone https://github.com/hypothesis/via.git
cd via
make services
make devdata
make help

To run Via locally run make dev and visit http://localhost:9083.

Changing the Project's Python Version

To change what version of Python the project uses:

  1. Change the Python version in the cookiecutter.json file. For example:

    "python_version": "3.10.4",
  2. Re-run the cookiecutter template:

    make template
    
  3. Re-compile the requirements/*.txt files. This is necessary because the same requirements/*.in file can compile to different requirements/*.txt files in different versions of Python:

    make requirements
    
  4. Commit everything to git and send a pull request

Changing the Project's Python Dependencies

To Add a New Dependency

Add the package to the appropriate requirements/*.in file(s) and then run:

make requirements

To Remove a Dependency

Remove the package from the appropriate requirements/*.in file(s) and then run:

make requirements

To Upgrade or Downgrade a Dependency

We rely on Dependabot to keep all our dependencies up to date by sending automated pull requests to all our repos. But if you need to upgrade or downgrade a package manually you can do that locally.

To upgrade a package to the latest version in all requirements/*.txt files:

make requirements --always-make args='--upgrade-package <FOO>'

To upgrade or downgrade a package to a specific version:

make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'

To upgrade all packages to their latest versions:

make requirements --always-make args=--upgrade

Configuration

Environment variables:

NamePurposeExample
CHECKMATE_URLThe URL of the URL Checkmate instance to usehttps://checkmate.example.com
CHECKMATE_API_KEYAPI key to authenticate with Checkmate
CHECKMATE_ALLOW_ALLWhether to bypass Checkmate's allow-list (and use only the blocklist)true
CHECKMATE_IGNORE_REASONSComma-separated list of Checkmate block reasons to ignorepublisher-blocked,high-io
CLIENT_EMBED_URLThe URL of the client's embed scripthttps://hypothes.is/embed.js
DATA_DIRECTORYDirectory for externally provided data/via-data
ENABLE_FRONT_PAGEShow a front page at the root URLtrue
NEW_RELIC_*Various New Relic settings. See New Relic's docs for details
NGINX_SECURE_LINK_SECRETThe NGINX secure links signing secret. This is used by Via's Python endpoints to generate the signed URLs required by its NGINX-implemented /proxy/static/ endpoint. All instances of Via must have this setting
NGINX_SERVERThe URL of Via's NGINX server for proxying PDF fileshttps://via.hypothes.is
SENTRY_*Various Sentry settings. See Sentry's docs for details
SIGNED_URLS_REQUIREDRequire URLs to Via's Python endpoints to be signed so that Via can only be used by something that has the URL signing secret. Public instances of Via should not enable this. Private instances of Via (e.g. the LMS app's instance of Via) should enable thistrue
VIA_HTML_URLThe URL of the Via HTML instance to redirect to for proxying HTML pageshttps://viahtml.hypothes.is/proxy
VIA_SECRETThe secret that must be used to sign URLs to Via's Python endpoints if SIGNED_URLS_REQUIRED is on

Expected data:

The following data is expected to be provided in the DATA_DIRECTORY:

  • google_drive_credentials.json - A list of credential JSON objects provided by the Google API console
  • google_drive_resource_keys.json - A dict of file ids to resource keys

Error codes

Most error codes have their natural meanings, but there are some special cases that we catch. Those marged with (error) are from our point of view something which is worth investigation. Others are a normal part of the day-to-day.

All end-points:

  • 401 - The user needs a secure token and either they don't have one, or it's invalid

General end-points:

  • 400 - The user has make a mistake in calling us or given us a bad URL
  • 408 - We timed out accessing a website
  • 409 - A website we have called gave us a conventional error like a connection error. Trying again could help.
  • 417 - A website has given us an unexpected response. This could be anything. Trying again could help.

/google_drive/*

  • 403 - The user has given us a URL which doesn't grant us permission to download it
  • 404 - The user has given us an invalid URL or one we really don't have permission to see
  • 408 (error) - We timed out trying to make a connection to Google
  • 409 (error) - We had a conventional error like a connection error
  • 417 (error) - We got an unexpected response from Google
  • 423 (error) - Google has blocked the file as malicous
  • 429 (error) - We have been rate limited by Google

Updating the PDF viewer

Via serves PDFs using PDF.js. PDF.js is vendored into the source tree and the viewer HTML is patched to load the Hypothesis client. To update the PDF viewer, run make update-pdfjs.

How Via works

Via allows users to annotate arbitrary web pages or PDF files by proxying the page or file and injecting the Hypothesis client. Users go to https://via.hypothes.is/ and paste in a PDF or HTML URL (or visit https://via.hypothes.is/<SOME_URL> directly) and Via responds with an annotatable version.

Via's architecture

Via is composed of four separable components:

  1. A top-level component that responds to requests to the top-level /<THIRD_PARTY_URL> endpoint by deciding whether the URL is a PDF file or not and redirecting the browser to either the PDF viewer component or the HTML proxying component accordingly.

    This component is implemented in Python / Pyramid.

    The Pyramid app sends a GET request to the third-party URL but only downloads the response headers not the body. It looks at the Content-Type header to determine whether the body is a PDF file or not.

    If it's a PDF file then it redirects to the PDF viewer component: /pdf/<THIRD_PARTY_URL>.

    If it's an HTML file then it redirects to the HTML proxy component.

    The Pyramid app also handles various other bits and bobs such as serving up the front page, handling special via.* query params, serving static files such as PDF.js's assets, etc etc.

  2. A PDF viewer component that renders a modified version of PDF.js with the Hypothesis client embedded.

    This is what enables users to annotate PDF files.

    The PDF viewer is also implemented in Python / Pyramid (and JavaScript served by the Pyramid app).

    The PDF viewer responds to requests to /pdf/<THIRD_PARTY_URL> by rendering a version of PDF.js with the Hypothesis client embedded, and configuring PDF.js to download the PDF file from the static file proxy component.

  3. A static files proxy component that simply proxies static files to get around CORS.

    This component is implemented in NGINX (in the nginx.conf file) for efficiency.

    This component responds to requests to the /proxy/static/<THIRD_PARTY_URL> endpoint, such as PDF.js's download requests for PDF files.

    Many PDF hosts use CORS headers to prevent JavaScript cross-origin requests (such as requests from our copy of PDF.js) from downloading the file. See Can I load a PDF from another server (cross domain request)? in the PDF.js FAQ.

    To get around this we proxy the PDF file through our own server so that browsers no longer see PDF.js's download request as a cross-origin request.

    In the future we'll also use this component to proxy some static resources of web pages for the same reason.

  4. A rewriting HTML proxy component that proxies HTML pages and injects the Hypothesis client.

    This is what enables users to annotate web pages.

    The HTML proxy isn't implemented yet. Via currently redirects to legacy Via for HTML proxying.

    The HTML proxy's job is to enable annotating of HTML pages by proxying the page and injecting the Hypothesis client into it.

    It also has to rewrite various elements of the page that would otherwise break because the page is being proxied.

How Via works in production

In production both NGINX and Gunicorn (the WSGI server for the Python / Pyramid app) run inside a single Docker container defined by the app's Dockerfile.

NGINX runs on port 9083 in the Docker container, which is exposed to the outside world.

Gunicorn runs on a UNIX socket that is accessible to NGINX within the Docker container but is not directly accessible to the outside world.

NGINX is "in front of" Gunicorn in production:

  1. All requests from user's browsers first go to NGINX on the Docker container's port 9083.

  2. If the request is to a URL that NGINX handles directly (such as a /proxy/static/* URL) then NGINX just responds directly.

  3. If the request is to one of the URLs that should be handled by the Pyramid app then NGINX proxies to Gunicorn on a UNIX socket.

How Via works in development

In development NGINX runs in Docker Compose and is exposed at http://localhost:9083/. This is defined in docker-compose.yml. The app's Dockerfile isn't used in development, but the NGINX running in Docker Compose in development does use the same nginx.conf file as the NGINX running in Docker in production.

The Python WSGI server (Gunicorn) runs on the host (no Docker) and is exposed at http://localhost:9082/. The NGINX running on :9083 proxies to the Gunicorn on :9082.

WhiteNoise

The Pyramid app uses WhiteNoise to serve static files in a CDN-friendly (caching-friendly) way. WhiteNoise serves the Python app's static files in an efficient way and with the appropriate caching headers, compression, etc.

WhiteNoise is a piece of WSGI middleware that wraps our Pyramid WSGI app. Rather than proxying to Pyramid directly Gunicorn actually proxies to WhiteNoise which either responds directly (if the request is for a static file) or proxies to Pyramid.

See also

About

Proxies third-party PDF files and HTML pages with the Hypothesis client embedded, so you can annotate them

Resources

Code of conduct

Stars

29 stars

Watchers

4 watching

Forks

Used by

Contributors

Languages